mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
ci(swift): enforce shared OpenClawKit dead-code coverage (#105770)
* ci(swift): enforce shared kit dead-code coverage * chore(i18n): sync native source inventory * ci(swift): install pinned iOS scan tools
This commit is contained in:
committed by
GitHub
parent
ff166425d1
commit
e01d1e85f3
@@ -0,0 +1,263 @@
|
||||
name: Shared OpenClawKit Periphery
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: shared-openclawkit-periphery-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
scope:
|
||||
name: Detect shared OpenClawKit scan scope
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
should-scan: ${{ steps.scope.outputs.should-scan }}
|
||||
steps:
|
||||
- name: Detect changed paths
|
||||
id: scope
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
core.setOutput("should-scan", "true");
|
||||
return;
|
||||
}
|
||||
if (context.payload.pull_request?.draft) {
|
||||
core.setOutput("should-scan", "false");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const isScanPath = (filename) =>
|
||||
typeof filename === "string" && (
|
||||
filename.startsWith("apps/ios/") ||
|
||||
filename.startsWith("apps/macos/") ||
|
||||
filename.startsWith("apps/shared/OpenClawKit/") ||
|
||||
filename === ".github/workflows/shared-openclawkit-periphery.yml" ||
|
||||
filename === "scripts/periphery-intersection.mjs" ||
|
||||
filename === "scripts/ios-configure-signing.sh" ||
|
||||
filename === "scripts/ios-write-swift-filelist.mjs" ||
|
||||
filename === "scripts/ios-write-version-xcconfig.sh" ||
|
||||
filename === "test/scripts/periphery-intersection.test.ts"
|
||||
);
|
||||
const shouldScan = files.some(
|
||||
({ filename, previous_filename: previousFilename }) =>
|
||||
isScanPath(filename) || isScanPath(previousFilename)
|
||||
);
|
||||
core.setOutput("should-scan", String(shouldScan));
|
||||
|
||||
scan-ios:
|
||||
name: Scan shared kit from iOS
|
||||
needs: scope
|
||||
if: ${{ needs.scope.outputs.should-scan == 'true' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Verify Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
|
||||
if [ -d "$xcode_app/Contents/Developer" ]; then
|
||||
sudo xcode-select -s "$xcode_app/Contents/Developer"
|
||||
break
|
||||
fi
|
||||
done
|
||||
xcodebuild -version
|
||||
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
|
||||
if [[ "$xcode_version" != 26.* ]]; then
|
||||
echo "error: expected Xcode 26.x, got $xcode_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
swift --version
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Install iOS scan tooling
|
||||
run: |
|
||||
brew update
|
||||
brew install xcodegen periphery
|
||||
swift_tools_dir="$RUNNER_TEMP/openclaw-swift-tools"
|
||||
./scripts/install-swift-tools.sh "$swift_tools_dir"
|
||||
echo "$swift_tools_dir" >> "$GITHUB_PATH"
|
||||
"$swift_tools_dir/swiftformat" --version
|
||||
"$swift_tools_dir/swiftlint" version
|
||||
|
||||
- name: Generate iOS project
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/ios-configure-signing.sh
|
||||
./scripts/ios-write-version-xcconfig.sh
|
||||
node scripts/ios-write-swift-filelist.mjs
|
||||
cd apps/ios
|
||||
xcodegen generate
|
||||
|
||||
- name: Scan shared kit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output_dir="$RUNNER_TEMP/shared-periphery-ios"
|
||||
mkdir -p "$output_dir"
|
||||
cd apps/ios
|
||||
set +e
|
||||
periphery scan \
|
||||
--config .periphery.yml \
|
||||
--clean-build \
|
||||
--format json \
|
||||
--report-include '../shared/OpenClawKit/Sources/**' \
|
||||
--retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift' \
|
||||
--write-results "$output_dir/periphery.json" \
|
||||
>"$output_dir/periphery.stdout.json" \
|
||||
2>"$output_dir/periphery.stderr.log"
|
||||
periphery_status="$?"
|
||||
set -e
|
||||
printf '%s\n' "$periphery_status" >"$output_dir/periphery.status"
|
||||
if [ ! -s "$output_dir/periphery.json" ]; then
|
||||
cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json"
|
||||
fi
|
||||
|
||||
- name: Upload iOS consumer report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: shared-periphery-ios-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-periphery-ios
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
scan-macos:
|
||||
name: Scan shared kit from macOS
|
||||
needs: scope
|
||||
if: ${{ needs.scope.outputs.should-scan == 'true' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Verify Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
|
||||
if [ -d "$xcode_app/Contents/Developer" ]; then
|
||||
sudo xcode-select -s "$xcode_app/Contents/Developer"
|
||||
break
|
||||
fi
|
||||
done
|
||||
xcodebuild -version
|
||||
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
|
||||
if [[ "$xcode_version" != 26.* ]]; then
|
||||
echo "error: expected Xcode 26.x, got $xcode_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
swift --version
|
||||
|
||||
- name: Install Periphery
|
||||
run: |
|
||||
brew update
|
||||
brew install periphery
|
||||
|
||||
- name: Scan shared kit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output_dir="$RUNNER_TEMP/shared-periphery-macos"
|
||||
mkdir -p "$output_dir"
|
||||
cd apps/macos
|
||||
set +e
|
||||
periphery scan \
|
||||
--config .periphery.yml \
|
||||
--clean-build \
|
||||
--format json \
|
||||
--report-include '../shared/OpenClawKit/Sources/**' \
|
||||
--retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift' \
|
||||
--write-results "$output_dir/periphery.json" \
|
||||
>"$output_dir/periphery.stdout.json" \
|
||||
2>"$output_dir/periphery.stderr.log"
|
||||
periphery_status="$?"
|
||||
set -e
|
||||
printf '%s\n' "$periphery_status" >"$output_dir/periphery.status"
|
||||
if [ ! -s "$output_dir/periphery.json" ]; then
|
||||
cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json"
|
||||
fi
|
||||
|
||||
- name: Upload macOS consumer report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: shared-periphery-macos-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-periphery-macos
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
intersect:
|
||||
name: Intersect shared OpenClawKit dead code
|
||||
needs: [scope, scan-ios, scan-macos]
|
||||
if: ${{ always() && needs.scope.outputs.should-scan == 'true' && needs.scan-ios.result != 'cancelled' && needs.scan-macos.result != 'cancelled' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Download iOS consumer report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: shared-periphery-ios-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-periphery-ios
|
||||
|
||||
- name: Download macOS consumer report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: shared-periphery-macos-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-periphery-macos
|
||||
|
||||
- name: Intersect exact Swift identities
|
||||
run: |
|
||||
node scripts/periphery-intersection.mjs \
|
||||
--ios-results "$RUNNER_TEMP/shared-periphery-ios/periphery.json" \
|
||||
--ios-status "$RUNNER_TEMP/shared-periphery-ios/periphery.status" \
|
||||
--macos-results "$RUNNER_TEMP/shared-periphery-macos/periphery.json" \
|
||||
--macos-status "$RUNNER_TEMP/shared-periphery-macos/periphery.status" \
|
||||
--output "$RUNNER_TEMP/shared-periphery-intersection/periphery.json"
|
||||
|
||||
- name: Upload shared intersection
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: shared-periphery-intersection-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-periphery-intersection
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
@@ -32707,7 +32707,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 547,
|
||||
"line": 548,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift",
|
||||
"source": "Image",
|
||||
"surface": "apple",
|
||||
@@ -33155,7 +33155,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1178,
|
||||
"line": 1179,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
|
||||
"source": "Remove attachments or wait for delivery to resolve before switching chats.",
|
||||
"surface": "apple",
|
||||
@@ -33163,7 +33163,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1234,
|
||||
"line": 1235,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
|
||||
"source": "Remove attachments or wait for delivery to resolve before starting a new chat.",
|
||||
"surface": "apple",
|
||||
@@ -33595,7 +33595,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 198,
|
||||
"line": 191,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift",
|
||||
"source": "\\(preview)…",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -17,6 +17,7 @@ public struct OpenClawChatHaptics: Sendable {
|
||||
self.performer = Self.defaultPerformer
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests inject a recorder; app consumers use the live performer.
|
||||
public init(performer: @escaping @Sendable (Event) -> Void) {
|
||||
self.performer = performer
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ struct ChatMarkdownProse {
|
||||
}
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests inspect parsed math spans without exposing renderer internals.
|
||||
var inlineMathLatex: [String] {
|
||||
self.inlineContent?.compactMap { content in
|
||||
if case let .math(span) = content {
|
||||
|
||||
@@ -376,6 +376,7 @@ public struct OpenClawChatInFlightRun: Codable, Sendable {
|
||||
public let runId: String
|
||||
public let text: String
|
||||
|
||||
// periphery:ignore - package tests construct history fixtures; app consumers decode this payload.
|
||||
public init(runId: String, text: String) {
|
||||
self.runId = runId
|
||||
self.text = text
|
||||
@@ -385,6 +386,7 @@ public struct OpenClawChatInFlightRun: Codable, Sendable {
|
||||
public struct OpenClawChatSessionInfo: Codable, Sendable {
|
||||
public let hasActiveRun: Bool?
|
||||
|
||||
// periphery:ignore - package tests construct history fixtures; app consumers decode this payload.
|
||||
public init(hasActiveRun: Bool?) {
|
||||
self.hasActiveRun = hasActiveRun
|
||||
}
|
||||
@@ -455,6 +457,7 @@ public struct OpenClawChatEventPayload: Codable, Sendable {
|
||||
public let message: AnyCodable?
|
||||
public let errorMessage: String?
|
||||
|
||||
// periphery:ignore - package tests construct transport events; app consumers decode them.
|
||||
public init(
|
||||
runId: String?,
|
||||
sessionKey: String?,
|
||||
@@ -479,6 +482,7 @@ public struct OpenClawSessionMessageEventPayload: Codable, Sendable {
|
||||
public let messageId: String?
|
||||
public let messageSeq: Int?
|
||||
|
||||
// periphery:ignore - package tests construct transport events; app consumers decode them.
|
||||
public init(
|
||||
sessionKey: String?,
|
||||
agentId: String? = nil,
|
||||
|
||||
@@ -59,6 +59,7 @@ public struct OpenClawChatModelPatchResult: Decodable, Sendable, Equatable {
|
||||
public let thinkingLevel: String?
|
||||
public let thinkingLevels: [OpenClawChatThinkingLevelOption]?
|
||||
|
||||
// periphery:ignore - package tests construct patch responses; app consumers decode them.
|
||||
public init(
|
||||
key: String? = nil,
|
||||
modelProvider: String?,
|
||||
|
||||
@@ -71,7 +71,7 @@ extension OpenClawChatTranscriptCache {
|
||||
/// Optional atomic merge seam for cache owners that also provide a durable
|
||||
/// outbox. Keeping this separate preserves source compatibility for read-only
|
||||
/// transcript-cache conformers.
|
||||
public protocol OpenClawChatCanonicalTranscriptMerging: OpenClawChatTranscriptCache {
|
||||
protocol OpenClawChatCanonicalTranscriptMerging: OpenClawChatTranscriptCache {
|
||||
func mergeCanonicalTranscriptMessage(
|
||||
sessionKey: String,
|
||||
agentID: String?,
|
||||
|
||||
@@ -455,6 +455,7 @@ public final class OpenClawChatViewModel {
|
||||
self.applySessionSwitch(to: sessionKey, intent: .externalSync)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests vary one identity field while preserving the current routing contract.
|
||||
public func syncActiveAgentId(_ agentId: String?) {
|
||||
self.syncDeliveryIdentity(
|
||||
activeAgentId: agentId,
|
||||
|
||||
@@ -98,9 +98,9 @@ public struct TalkWaveformView: View {
|
||||
|
||||
/// Pure waveform math, split from the view for unit testing and so the Android
|
||||
/// port has one canonical reference for every constant.
|
||||
public enum TalkWaveformMath {
|
||||
enum TalkWaveformMath {
|
||||
/// Per-phase drive for the wave amplitude in 0...1.
|
||||
public static func power(for phase: TalkWaveformPhase, time: Double) -> Double {
|
||||
static func power(for phase: TalkWaveformPhase, time: Double) -> Double {
|
||||
switch phase {
|
||||
case .idle:
|
||||
return 0.05
|
||||
@@ -121,7 +121,7 @@ public enum TalkWaveformMath {
|
||||
}
|
||||
|
||||
/// One wave = max envelope of three drifting lobes, mirrored around the midline.
|
||||
public static func wavePath(in size: CGSize, time: Double, seed: Double, power: Double) -> Path {
|
||||
static func wavePath(in size: CGSize, time: Double, seed: Double, power: Double) -> Path {
|
||||
let midX = Double(size.width) / 2
|
||||
let midY = Double(size.height) / 2
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
public struct BridgeBaseFrame: Codable, Sendable {
|
||||
public let type: String
|
||||
|
||||
public init(type: String) {
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeInvokeRequest: Codable, Sendable {
|
||||
public let type: String
|
||||
public let id: String
|
||||
@@ -54,216 +46,3 @@ public struct BridgeInvokeResponse: Codable, Sendable {
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeEventFrame: Codable, Sendable {
|
||||
public let type: String
|
||||
public let event: String
|
||||
public let payloadJSON: String?
|
||||
|
||||
public init(type: String = "event", event: String, payloadJSON: String? = nil) {
|
||||
self.type = type
|
||||
self.event = event
|
||||
self.payloadJSON = payloadJSON
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeHello: Codable, Sendable {
|
||||
public let type: String
|
||||
public let nodeId: String
|
||||
public let displayName: String?
|
||||
public let token: String?
|
||||
public let platform: String?
|
||||
public let version: String?
|
||||
public let coreVersion: String?
|
||||
public let uiVersion: String?
|
||||
public let deviceFamily: String?
|
||||
public let modelIdentifier: String?
|
||||
public let caps: [String]?
|
||||
public let commands: [String]?
|
||||
public let permissions: [String: Bool]?
|
||||
|
||||
public init(
|
||||
type: String = "hello",
|
||||
nodeId: String,
|
||||
displayName: String?,
|
||||
token: String?,
|
||||
platform: String?,
|
||||
version: String?,
|
||||
coreVersion: String? = nil,
|
||||
uiVersion: String? = nil,
|
||||
deviceFamily: String? = nil,
|
||||
modelIdentifier: String? = nil,
|
||||
caps: [String]? = nil,
|
||||
commands: [String]? = nil,
|
||||
permissions: [String: Bool]? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.nodeId = nodeId
|
||||
self.displayName = displayName
|
||||
self.token = token
|
||||
self.platform = platform
|
||||
self.version = version
|
||||
self.coreVersion = coreVersion
|
||||
self.uiVersion = uiVersion
|
||||
self.deviceFamily = deviceFamily
|
||||
self.modelIdentifier = modelIdentifier
|
||||
self.caps = caps
|
||||
self.commands = commands
|
||||
self.permissions = permissions
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeHelloOk: Codable, Sendable {
|
||||
public let type: String
|
||||
public let serverName: String
|
||||
public let mainSessionKey: String?
|
||||
|
||||
public init(
|
||||
type: String = "hello-ok",
|
||||
serverName: String,
|
||||
mainSessionKey: String? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.serverName = serverName
|
||||
self.mainSessionKey = mainSessionKey
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgePairRequest: Codable, Sendable {
|
||||
public let type: String
|
||||
public let nodeId: String
|
||||
public let displayName: String?
|
||||
public let platform: String?
|
||||
public let version: String?
|
||||
public let coreVersion: String?
|
||||
public let uiVersion: String?
|
||||
public let deviceFamily: String?
|
||||
public let modelIdentifier: String?
|
||||
public let caps: [String]?
|
||||
public let commands: [String]?
|
||||
public let permissions: [String: Bool]?
|
||||
public let remoteAddress: String?
|
||||
public let silent: Bool?
|
||||
|
||||
public init(
|
||||
type: String = "pair-request",
|
||||
nodeId: String,
|
||||
displayName: String?,
|
||||
platform: String?,
|
||||
version: String?,
|
||||
coreVersion: String? = nil,
|
||||
uiVersion: String? = nil,
|
||||
deviceFamily: String? = nil,
|
||||
modelIdentifier: String? = nil,
|
||||
caps: [String]? = nil,
|
||||
commands: [String]? = nil,
|
||||
permissions: [String: Bool]? = nil,
|
||||
remoteAddress: String? = nil,
|
||||
silent: Bool? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.nodeId = nodeId
|
||||
self.displayName = displayName
|
||||
self.platform = platform
|
||||
self.version = version
|
||||
self.coreVersion = coreVersion
|
||||
self.uiVersion = uiVersion
|
||||
self.deviceFamily = deviceFamily
|
||||
self.modelIdentifier = modelIdentifier
|
||||
self.caps = caps
|
||||
self.commands = commands
|
||||
self.permissions = permissions
|
||||
self.remoteAddress = remoteAddress
|
||||
self.silent = silent
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgePairOk: Codable, Sendable {
|
||||
public let type: String
|
||||
public let token: String
|
||||
|
||||
public init(type: String = "pair-ok", token: String) {
|
||||
self.type = type
|
||||
self.token = token
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgePing: Codable, Sendable {
|
||||
public let type: String
|
||||
public let id: String
|
||||
|
||||
public init(type: String = "ping", id: String) {
|
||||
self.type = type
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgePong: Codable, Sendable {
|
||||
public let type: String
|
||||
public let id: String
|
||||
|
||||
public init(type: String = "pong", id: String) {
|
||||
self.type = type
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeErrorFrame: Codable, Sendable {
|
||||
public let type: String
|
||||
public let code: String
|
||||
public let message: String
|
||||
|
||||
public init(type: String = "error", code: String, message: String) {
|
||||
self.type = type
|
||||
self.code = code
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Optional RPC (node -> bridge)
|
||||
|
||||
public struct BridgeRPCRequest: Codable, Sendable {
|
||||
public let type: String
|
||||
public let id: String
|
||||
public let method: String
|
||||
public let paramsJSON: String?
|
||||
|
||||
public init(type: String = "req", id: String, method: String, paramsJSON: String? = nil) {
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.method = method
|
||||
self.paramsJSON = paramsJSON
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeRPCError: Codable, Sendable, Equatable {
|
||||
public let code: String
|
||||
public let message: String
|
||||
|
||||
public init(code: String, message: String) {
|
||||
self.code = code
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
public struct BridgeRPCResponse: Codable, Sendable {
|
||||
public let type: String
|
||||
public let id: String
|
||||
public let ok: Bool
|
||||
public let payloadJSON: String?
|
||||
public let error: BridgeRPCError?
|
||||
|
||||
public init(
|
||||
type: String = "res",
|
||||
id: String,
|
||||
ok: Bool,
|
||||
payloadJSON: String? = nil,
|
||||
error: BridgeRPCError? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.ok = ok
|
||||
self.payloadJSON = payloadJSON
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,28 +72,6 @@ public enum CameraCapturePipelineSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps the flat overload source-compatible while the options form owns the implementation.
|
||||
public static func prepareMovieSession(
|
||||
preferFrontCamera: Bool,
|
||||
deviceId: String?,
|
||||
includeAudio: Bool,
|
||||
durationMs: Int,
|
||||
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
|
||||
cameraUnavailableError: @autoclosure () -> Error,
|
||||
mapSetupError: (CameraSessionConfigurationError) -> Error) throws
|
||||
-> (session: AVCaptureSession, output: AVCaptureMovieFileOutput)
|
||||
{
|
||||
try self.prepareMovieSession(
|
||||
options: CameraMovieSessionOptions(
|
||||
preferFrontCamera: preferFrontCamera,
|
||||
deviceId: deviceId,
|
||||
includeAudio: includeAudio,
|
||||
durationMs: durationMs),
|
||||
pickCamera: pickCamera,
|
||||
cameraUnavailableError: cameraUnavailableError(),
|
||||
mapSetupError: mapSetupError)
|
||||
}
|
||||
|
||||
public static func prepareWarmMovieSession(
|
||||
options: CameraMovieSessionOptions,
|
||||
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
|
||||
@@ -119,28 +97,6 @@ public enum CameraCapturePipelineSupport {
|
||||
return prepared
|
||||
}
|
||||
|
||||
/// Keeps the flat overload source-compatible while the options form owns the implementation.
|
||||
public static func prepareWarmMovieSession(
|
||||
preferFrontCamera: Bool,
|
||||
deviceId: String?,
|
||||
includeAudio: Bool,
|
||||
durationMs: Int,
|
||||
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
|
||||
cameraUnavailableError: @autoclosure () -> Error,
|
||||
mapSetupError: (CameraSessionConfigurationError) -> Error) async throws
|
||||
-> (session: AVCaptureSession, output: AVCaptureMovieFileOutput)
|
||||
{
|
||||
try await self.prepareWarmMovieSession(
|
||||
options: CameraMovieSessionOptions(
|
||||
preferFrontCamera: preferFrontCamera,
|
||||
deviceId: deviceId,
|
||||
includeAudio: includeAudio,
|
||||
durationMs: durationMs),
|
||||
pickCamera: pickCamera,
|
||||
cameraUnavailableError: cameraUnavailableError(),
|
||||
mapSetupError: mapSetupError)
|
||||
}
|
||||
|
||||
public static func withWarmMovieSession<T>(
|
||||
options: CameraMovieSessionOptions,
|
||||
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
|
||||
@@ -161,29 +117,6 @@ public enum CameraCapturePipelineSupport {
|
||||
operation: { try await operation(prepared.output) })
|
||||
}
|
||||
|
||||
/// Keeps the flat overload source-compatible while the options form owns the implementation.
|
||||
public static func withWarmMovieSession<T>(
|
||||
preferFrontCamera: Bool,
|
||||
deviceId: String? = nil,
|
||||
includeAudio: Bool,
|
||||
durationMs: Int,
|
||||
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
|
||||
cameraUnavailableError: @autoclosure () -> Error,
|
||||
mapSetupError: (CameraSessionConfigurationError) -> Error,
|
||||
operation: (AVCaptureMovieFileOutput) async throws -> T) async throws -> T
|
||||
{
|
||||
try await self.withWarmMovieSession(
|
||||
options: CameraMovieSessionOptions(
|
||||
preferFrontCamera: preferFrontCamera,
|
||||
deviceId: deviceId,
|
||||
includeAudio: includeAudio,
|
||||
durationMs: durationMs),
|
||||
pickCamera: pickCamera,
|
||||
cameraUnavailableError: cameraUnavailableError(),
|
||||
mapSetupError: mapSetupError,
|
||||
operation: operation)
|
||||
}
|
||||
|
||||
static func withCaptureSessionLifecycle<T>(
|
||||
start: () -> Void,
|
||||
stop: () -> Void,
|
||||
|
||||
@@ -25,8 +25,8 @@ public enum CameraSessionConfigurationError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
public enum CameraSessionConfiguration {
|
||||
public static func addCameraInput(session: AVCaptureSession, camera: AVCaptureDevice) throws {
|
||||
enum CameraSessionConfiguration {
|
||||
static func addCameraInput(session: AVCaptureSession, camera: AVCaptureDevice) throws {
|
||||
let input = try AVCaptureDeviceInput(device: camera)
|
||||
guard session.canAddInput(input) else {
|
||||
throw CameraSessionConfigurationError.addCameraInputFailed
|
||||
@@ -34,7 +34,7 @@ public enum CameraSessionConfiguration {
|
||||
session.addInput(input)
|
||||
}
|
||||
|
||||
public static func addPhotoOutput(session: AVCaptureSession) throws -> AVCapturePhotoOutput {
|
||||
static func addPhotoOutput(session: AVCaptureSession) throws -> AVCapturePhotoOutput {
|
||||
let output = AVCapturePhotoOutput()
|
||||
guard session.canAddOutput(output) else {
|
||||
throw CameraSessionConfigurationError.addPhotoOutputFailed
|
||||
@@ -44,7 +44,7 @@ public enum CameraSessionConfiguration {
|
||||
return output
|
||||
}
|
||||
|
||||
public static func addMovieOutput(
|
||||
static func addMovieOutput(
|
||||
session: AVCaptureSession,
|
||||
includeAudio: Bool,
|
||||
durationMs: Int) throws -> AVCaptureMovieFileOutput
|
||||
|
||||
@@ -11,10 +11,6 @@ public enum OpenClawCanvasA2UICommand: String, Codable, Sendable {
|
||||
|
||||
public struct OpenClawCanvasA2UIPushParams: Codable, Sendable, Equatable {
|
||||
public var messages: [AnyCodable]
|
||||
|
||||
public init(messages: [AnyCodable]) {
|
||||
self.messages = messages
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCanvasA2UIPushJSONLParams: Codable, Sendable, Equatable {
|
||||
|
||||
@@ -13,13 +13,6 @@ public struct OpenClawCanvasPlacement: Codable, Sendable, Equatable {
|
||||
public var y: Double?
|
||||
public var width: Double?
|
||||
public var height: Double?
|
||||
|
||||
public init(x: Double? = nil, y: Double? = nil, width: Double? = nil, height: Double? = nil) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCanvasPresentParams: Codable, Sendable, Equatable {
|
||||
@@ -67,10 +60,4 @@ public struct OpenClawCanvasSnapshotParams: Codable, Sendable, Equatable {
|
||||
public var maxWidth: Int?
|
||||
public var quality: Double?
|
||||
public var format: OpenClawCanvasSnapshotFormat?
|
||||
|
||||
public init(maxWidth: Int? = nil, quality: Double? = nil, format: OpenClawCanvasSnapshotFormat? = nil) {
|
||||
self.maxWidth = maxWidth
|
||||
self.quality = quality
|
||||
self.format = format
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,27 +61,6 @@ public enum GatewayDeviceAuthPayload {
|
||||
].joined(separator: "|")
|
||||
}
|
||||
|
||||
/// Keeps the flat overload source-compatible while `Fields` owns canonical serialization.
|
||||
public static func buildConnectCompatibilityPayload(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: [String],
|
||||
signedAtMs: Int64,
|
||||
token: String? = nil,
|
||||
nonce: String) -> String
|
||||
{
|
||||
self.buildConnectCompatibilityPayload(fields: Fields(
|
||||
deviceId: deviceId,
|
||||
client: Client(id: clientId, mode: clientMode),
|
||||
role: role,
|
||||
scopes: scopes,
|
||||
signedAtMs: signedAtMs,
|
||||
token: token,
|
||||
nonce: nonce))
|
||||
}
|
||||
|
||||
public static func buildV3(
|
||||
fields: Fields,
|
||||
platform: String?,
|
||||
@@ -106,32 +85,6 @@ public enum GatewayDeviceAuthPayload {
|
||||
].joined(separator: "|")
|
||||
}
|
||||
|
||||
/// Keeps the flat overload source-compatible while `Fields` owns canonical serialization.
|
||||
public static func buildV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: [String],
|
||||
signedAtMs: Int64,
|
||||
token: String? = nil,
|
||||
nonce: String,
|
||||
platform: String? = nil,
|
||||
deviceFamily: String? = nil) -> String
|
||||
{
|
||||
self.buildV3(
|
||||
fields: Fields(
|
||||
deviceId: deviceId,
|
||||
client: Client(id: clientId, mode: clientMode),
|
||||
role: role,
|
||||
scopes: scopes,
|
||||
signedAtMs: signedAtMs,
|
||||
token: token,
|
||||
nonce: nonce),
|
||||
platform: platform,
|
||||
deviceFamily: deviceFamily)
|
||||
}
|
||||
|
||||
static func normalizeMetadataField(_ value: String?) -> String {
|
||||
guard let value else { return "" }
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#if Talk && canImport(ElevenLabsKit)
|
||||
@_exported import ElevenLabsKit
|
||||
|
||||
public typealias ElevenLabsVoice = ElevenLabsKit.ElevenLabsVoice
|
||||
public typealias ElevenLabsTTSRequest = ElevenLabsKit.ElevenLabsTTSRequest
|
||||
public typealias ElevenLabsTTSClient = ElevenLabsKit.ElevenLabsTTSClient
|
||||
public typealias TalkTTSValidation = ElevenLabsKit.TalkTTSValidation
|
||||
|
||||
@@ -681,17 +681,6 @@ public actor GatewayNodeSession {
|
||||
}
|
||||
}
|
||||
|
||||
public func send(method: String, paramsJSON: String?) async throws {
|
||||
guard let channel else {
|
||||
throw NSError(domain: "Gateway", code: 11, userInfo: [
|
||||
NSLocalizedDescriptionKey: "not connected",
|
||||
])
|
||||
}
|
||||
|
||||
let params = try decodeParamsJSON(paramsJSON)
|
||||
try await channel.send(method: method, params: params)
|
||||
}
|
||||
|
||||
public func request(
|
||||
method: String,
|
||||
paramsJSON: String?,
|
||||
@@ -1086,14 +1075,17 @@ extension GatewayNodeSession {
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// periphery:ignore - package tests observe admission rollover without exposing mutable state.
|
||||
func _test_admissionGeneration() -> UInt64 {
|
||||
self.admissionGeneration
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests drive the private connection callback deterministically.
|
||||
func _test_notifyConnectedIfNeeded(admissionGeneration: UInt64) async {
|
||||
await self.notifyConnectedIfNeeded(admissionGeneration: admissionGeneration)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests inject gateway pushes without a live socket.
|
||||
func _test_handlePush(_ push: GatewayPush, socketGeneration: UInt64) async {
|
||||
await self.handlePush(
|
||||
push,
|
||||
@@ -1101,6 +1093,7 @@ extension GatewayNodeSession {
|
||||
socketGeneration: socketGeneration)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests inject socket retirement without a live channel.
|
||||
func _test_handleChannelDisconnected(_ reason: String, socketGeneration: UInt64) async {
|
||||
await self.handleChannelDisconnected(
|
||||
reason,
|
||||
@@ -1108,6 +1101,7 @@ extension GatewayNodeSession {
|
||||
socketGeneration: socketGeneration)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests verify event stream filtering without a live gateway.
|
||||
func _test_broadcastServerEvent(_ event: EventFrame) {
|
||||
self.broadcastServerEvent(event)
|
||||
}
|
||||
@@ -1247,6 +1241,7 @@ extension GatewayNodeSession {
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// periphery:ignore - package tests exercise receipt dedupe around the private invoke path.
|
||||
func invokeComputerWithReceiptForTesting(
|
||||
requestId: String,
|
||||
paramsJSON: String,
|
||||
@@ -1275,6 +1270,7 @@ extension GatewayNodeSession {
|
||||
onInvoke: onInvoke)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests assert receipt joining without exposing the receipt store.
|
||||
func computerReceiptJoinCountForTesting(
|
||||
idempotencyKey: String,
|
||||
receiptScope: String) -> Int
|
||||
|
||||
@@ -9,12 +9,4 @@ public enum GatewayPayloadDecoding {
|
||||
let data = try JSONEncoder().encode(payload)
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
public static func decodeIfPresent<T: Decodable>(
|
||||
_ payload: AnyCodable?,
|
||||
as _: T.Type = T.self) throws -> T?
|
||||
{
|
||||
guard let payload else { return nil }
|
||||
return try self.decode(payload, as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ public struct GatewayTLSValidationError: LocalizedError, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol GatewayTLSFailureProviding: AnyObject {
|
||||
protocol GatewayTLSFailureProviding: AnyObject {
|
||||
func consumeLastTLSFailure() -> GatewayTLSValidationFailure?
|
||||
}
|
||||
|
||||
public protocol GatewayDeviceTokenRetryTrustProviding: AnyObject {
|
||||
protocol GatewayDeviceTokenRetryTrustProviding: AnyObject {
|
||||
var allowsDeviceTokenRetryAuth: Bool { get }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import Foundation
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public enum JPEGTranscodeError: LocalizedError, Sendable {
|
||||
enum JPEGTranscodeError: LocalizedError, Sendable {
|
||||
case decodeFailed
|
||||
case propertiesMissing
|
||||
case encodeFailed
|
||||
case sizeLimitExceeded(maxBytes: Int, actualBytes: Int)
|
||||
|
||||
public var errorDescription: String? {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .decodeFailed:
|
||||
"Failed to decode image data"
|
||||
@@ -23,8 +23,8 @@ public enum JPEGTranscodeError: LocalizedError, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct JPEGTranscoder: Sendable {
|
||||
public static func clampQuality(_ quality: Double) -> Double {
|
||||
struct JPEGTranscoder: Sendable {
|
||||
static func clampQuality(_ quality: Double) -> Double {
|
||||
min(1.0, max(0.05, quality))
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public struct JPEGTranscoder: Sendable {
|
||||
///
|
||||
/// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not
|
||||
/// relied on).
|
||||
public static func transcodeToJPEG(
|
||||
static func transcodeToJPEG(
|
||||
imageData: Data,
|
||||
maxWidthPx: Int?,
|
||||
quality: Double,
|
||||
@@ -51,7 +51,7 @@ public struct JPEGTranscoder: Sendable {
|
||||
/// When `maxLongEdgePx` is provided it takes precedence over `maxWidthPx`.
|
||||
/// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not
|
||||
/// relied on).
|
||||
public static func transcodeToJPEG(
|
||||
static func transcodeToJPEG(
|
||||
imageData: Data,
|
||||
maxWidthPx: Int? = nil,
|
||||
maxLongEdgePx: Int?,
|
||||
|
||||
@@ -25,12 +25,6 @@ extension LocationServiceCommon {
|
||||
public func accuracyAuthorization() -> CLAccuracyAuthorization {
|
||||
LocationServiceSupport.accuracyAuthorization(manager: self.locationManager)
|
||||
}
|
||||
|
||||
public func requestLocationOnce() async throws -> CLLocation {
|
||||
try await LocationServiceSupport.requestLocation(manager: self.locationManager) { continuation in
|
||||
self.locationRequestContinuation = continuation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConcurrentLocationServiceCommon {
|
||||
@@ -61,8 +55,8 @@ extension ConcurrentLocationServiceCommon {
|
||||
}
|
||||
}
|
||||
|
||||
public enum LocationServiceSupport {
|
||||
public static func accuracyAuthorization(manager: CLLocationManager) -> CLAccuracyAuthorization {
|
||||
enum LocationServiceSupport {
|
||||
static func accuracyAuthorization(manager: CLLocationManager) -> CLAccuracyAuthorization {
|
||||
if #available(iOS 14.0, macOS 11.0, *) {
|
||||
return manager.accuracyAuthorization
|
||||
}
|
||||
@@ -70,7 +64,7 @@ public enum LocationServiceSupport {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public static func requestLocation(
|
||||
static func requestLocation(
|
||||
manager: CLLocationManager,
|
||||
setContinuation: @escaping (CheckedContinuation<CLLocation, Error>) -> Void) async throws -> CLLocation
|
||||
{
|
||||
|
||||
@@ -10,25 +10,6 @@ public enum OpenClawScreenSnapshotFormat: String, Codable, Sendable {
|
||||
case png
|
||||
}
|
||||
|
||||
public struct OpenClawScreenSnapshotParams: Codable, Sendable, Equatable {
|
||||
public var screenIndex: Int?
|
||||
public var maxWidth: Int?
|
||||
public var quality: Double?
|
||||
public var format: OpenClawScreenSnapshotFormat?
|
||||
|
||||
public init(
|
||||
screenIndex: Int? = nil,
|
||||
maxWidth: Int? = nil,
|
||||
quality: Double? = nil,
|
||||
format: OpenClawScreenSnapshotFormat? = nil)
|
||||
{
|
||||
self.screenIndex = screenIndex
|
||||
self.maxWidth = maxWidth
|
||||
self.quality = quality
|
||||
self.format = format
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawScreenRecordParams: Codable, Sendable, Equatable {
|
||||
public var screenIndex: Int?
|
||||
public var durationMs: Int?
|
||||
|
||||
@@ -12,15 +12,11 @@ public struct OpenClawSessionsCompactResponse: Decodable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable {
|
||||
public let reason: String?
|
||||
struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable {
|
||||
let reason: String?
|
||||
|
||||
public var errorDescription: String? {
|
||||
var errorDescription: String? {
|
||||
let detail = self.reason?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return detail?.isEmpty == false ? detail : "Session compaction failed"
|
||||
}
|
||||
|
||||
public init(reason: String?) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,103 +24,6 @@ public enum OpenClawNotificationDelivery: String, Codable, Sendable {
|
||||
case auto
|
||||
}
|
||||
|
||||
public struct OpenClawSystemRunApprovalFileOperand: Codable, Sendable, Equatable {
|
||||
public var argvIndex: Int
|
||||
public var path: String
|
||||
public var sha256: String
|
||||
|
||||
public init(argvIndex: Int, path: String, sha256: String) {
|
||||
self.argvIndex = argvIndex
|
||||
self.path = path
|
||||
self.sha256 = sha256
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawSystemRunApprovalPlan: Codable, Sendable, Equatable {
|
||||
public var argv: [String]
|
||||
public var cwd: String?
|
||||
public var commandText: String
|
||||
public var commandPreview: String?
|
||||
public var agentId: String?
|
||||
public var sessionKey: String?
|
||||
public var policySnapshot: OpenClawSystemRunApprovalPolicySnapshot?
|
||||
public var mutableFileOperand: OpenClawSystemRunApprovalFileOperand?
|
||||
|
||||
public init(
|
||||
argv: [String],
|
||||
cwd: String?,
|
||||
commandText: String,
|
||||
commandPreview: String? = nil,
|
||||
agentId: String?,
|
||||
sessionKey: String?,
|
||||
policySnapshot: OpenClawSystemRunApprovalPolicySnapshot? = nil,
|
||||
mutableFileOperand: OpenClawSystemRunApprovalFileOperand? = nil)
|
||||
{
|
||||
self.argv = argv
|
||||
self.cwd = cwd
|
||||
self.commandText = commandText
|
||||
self.commandPreview = commandPreview
|
||||
self.agentId = agentId
|
||||
self.sessionKey = sessionKey
|
||||
self.policySnapshot = policySnapshot
|
||||
self.mutableFileOperand = mutableFileOperand
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawSystemRunParams: Codable, Sendable, Equatable {
|
||||
public var command: [String]
|
||||
public var rawCommand: String?
|
||||
public var cwd: String?
|
||||
public var env: [String: String]?
|
||||
public var timeoutMs: Int?
|
||||
public var needsScreenRecording: Bool?
|
||||
public var agentId: String?
|
||||
public var sessionKey: String?
|
||||
public var runId: String?
|
||||
public var systemRunPlan: OpenClawSystemRunApprovalPlan?
|
||||
public var approved: Bool?
|
||||
public var approvalDecision: String?
|
||||
public var approvalSource: String?
|
||||
|
||||
public init(
|
||||
command: [String],
|
||||
rawCommand: String? = nil,
|
||||
cwd: String? = nil,
|
||||
env: [String: String]? = nil,
|
||||
timeoutMs: Int? = nil,
|
||||
needsScreenRecording: Bool? = nil,
|
||||
agentId: String? = nil,
|
||||
sessionKey: String? = nil,
|
||||
runId: String? = nil,
|
||||
systemRunPlan: OpenClawSystemRunApprovalPlan? = nil,
|
||||
approved: Bool? = nil,
|
||||
approvalDecision: String? = nil,
|
||||
approvalSource: String? = nil)
|
||||
{
|
||||
self.command = command
|
||||
self.rawCommand = rawCommand
|
||||
self.cwd = cwd
|
||||
self.env = env
|
||||
self.timeoutMs = timeoutMs
|
||||
self.needsScreenRecording = needsScreenRecording
|
||||
self.agentId = agentId
|
||||
self.sessionKey = sessionKey
|
||||
self.runId = runId
|
||||
self.systemRunPlan = systemRunPlan
|
||||
self.approved = approved
|
||||
self.approvalDecision = approvalDecision
|
||||
self.approvalSource = approvalSource
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawSystemWhichParams: Codable, Sendable, Equatable {
|
||||
public var bins: [String]
|
||||
|
||||
public init(bins: [String]) {
|
||||
self.bins = bins
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawSystemNotifyParams: Codable, Sendable, Equatable {
|
||||
public var title: String
|
||||
public var body: String
|
||||
|
||||
@@ -181,16 +181,16 @@ public final class PCMPlaybackEnvelope {
|
||||
/// via its built-in metering at ~30 Hz. Detach clears the level to nil so the
|
||||
/// consumer can distinguish "silent" from "not playing".
|
||||
@MainActor
|
||||
public final class AudioPlayerLevelMeter {
|
||||
final class AudioPlayerLevelMeter {
|
||||
private let onLevel: @MainActor (Double?) -> Void
|
||||
private var pollTask: Task<Void, Never>?
|
||||
private weak var player: AVAudioPlayer?
|
||||
|
||||
public init(onLevel: @escaping @MainActor (Double?) -> Void) {
|
||||
init(onLevel: @escaping @MainActor (Double?) -> Void) {
|
||||
self.onLevel = onLevel
|
||||
}
|
||||
|
||||
public func attach(_ player: AVAudioPlayer) {
|
||||
func attach(_ player: AVAudioPlayer) {
|
||||
self.detach()
|
||||
player.isMeteringEnabled = true
|
||||
self.player = player
|
||||
@@ -204,7 +204,7 @@ public final class AudioPlayerLevelMeter {
|
||||
}
|
||||
}
|
||||
|
||||
public func detach() {
|
||||
func detach() {
|
||||
self.pollTask?.cancel()
|
||||
self.pollTask = nil
|
||||
self.player = nil
|
||||
|
||||
@@ -16,10 +16,6 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
|
||||
private var currentToken = UUID()
|
||||
private var watchdog: Task<Void, Never>?
|
||||
|
||||
public var isSpeaking: Bool {
|
||||
self.synth.isSpeaking
|
||||
}
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
self.synth.delegate = self
|
||||
|
||||
@@ -14,13 +14,6 @@ public struct ToolDisplaySummary: Sendable, Equatable {
|
||||
if let detail, !detail.isEmpty { parts.append(detail) }
|
||||
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
public var summaryLine: String {
|
||||
if let detailLine {
|
||||
return "\(self.emoji) \(self.label): \(detailLine)"
|
||||
}
|
||||
return "\(self.emoji) \(self.label)"
|
||||
}
|
||||
}
|
||||
|
||||
public enum ToolDisplayRegistry {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extension WakeParams {
|
||||
// periphery:ignore - Shipped before sessionKey; remove only at a breaking protocol API window.
|
||||
public init(
|
||||
mode: AnyCodable,
|
||||
text: String)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import OpenClawKit
|
||||
import CoreGraphics
|
||||
import ImageIO
|
||||
import Testing
|
||||
import UniformTypeIdentifiers
|
||||
@testable import OpenClawKit
|
||||
|
||||
@Suite struct JPEGTranscoderTests {
|
||||
struct JPEGTranscoderTests {
|
||||
private func makeSolidJPEG(width: Int, height: Int, orientation: Int? = nil) throws -> Data {
|
||||
let cs = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
@@ -54,7 +54,7 @@ import UniformTypeIdentifiers
|
||||
let cs = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
|
||||
let out = try data.withUnsafeMutableBytes { rawBuffer -> Data in
|
||||
return try data.withUnsafeMutableBytes { rawBuffer -> Data in
|
||||
guard let base = rawBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
throw NSError(domain: "JPEGTranscoderTests", code: 6)
|
||||
}
|
||||
@@ -90,11 +90,9 @@ import UniformTypeIdentifiers
|
||||
}
|
||||
return encoded as Data
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@Test func downscalesToMaxWidthPx() throws {
|
||||
@Test func `downscales to max width px`() throws {
|
||||
let input = try makeSolidJPEG(width: 2000, height: 1000)
|
||||
let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9)
|
||||
#expect(out.widthPx == 1600)
|
||||
@@ -102,14 +100,14 @@ import UniformTypeIdentifiers
|
||||
#expect(out.data.count > 0)
|
||||
}
|
||||
|
||||
@Test func doesNotUpscaleWhenSmallerThanMaxWidthPx() throws {
|
||||
@Test func `does not upscale when smaller than max width px`() throws {
|
||||
let input = try makeSolidJPEG(width: 800, height: 600)
|
||||
let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9)
|
||||
#expect(out.widthPx == 800)
|
||||
#expect(out.heightPx == 600)
|
||||
}
|
||||
|
||||
@Test func normalizesOrientationAndUsesOrientedWidthForMaxWidthPx() throws {
|
||||
@Test func `normalizes orientation and uses oriented width for max width px`() throws {
|
||||
// Encode a landscape image but mark it rotated 90° (orientation 6). Oriented width becomes 1000.
|
||||
let input = try makeSolidJPEG(width: 2000, height: 1000, orientation: 6)
|
||||
let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9)
|
||||
@@ -117,7 +115,7 @@ import UniformTypeIdentifiers
|
||||
#expect(out.heightPx == 2000)
|
||||
}
|
||||
|
||||
@Test func respectsMaxBytes() throws {
|
||||
@Test func `respects max bytes`() throws {
|
||||
let input = try makeNoiseJPEG(width: 1600, height: 1200)
|
||||
let out = try JPEGTranscoder.transcodeToJPEG(
|
||||
imageData: input,
|
||||
@@ -127,7 +125,7 @@ import UniformTypeIdentifiers
|
||||
#expect(out.data.count <= 180_000)
|
||||
}
|
||||
|
||||
@Test func explicitlyFailsWhenSizeLimitCannotBeMet() throws {
|
||||
@Test func `explicitly fails when size limit cannot be met`() throws {
|
||||
let input = try makeSolidJPEG(width: 800, height: 600)
|
||||
|
||||
do {
|
||||
@@ -137,7 +135,7 @@ import UniformTypeIdentifiers
|
||||
quality: 0.9,
|
||||
maxBytes: 1)
|
||||
Issue.record("Expected a size-limit error")
|
||||
} catch JPEGTranscodeError.sizeLimitExceeded(let maxBytes, let actualBytes) {
|
||||
} catch let JPEGTranscodeError.sizeLimitExceeded(maxBytes, actualBytes) {
|
||||
#expect(maxBytes == 1)
|
||||
#expect(actualBytes > maxBytes)
|
||||
} catch {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
@testable import OpenClawKit
|
||||
|
||||
struct SessionMutationResponsesTests {
|
||||
@Test
|
||||
func compactResponseAcceptsSuccess() throws {
|
||||
func `compact response accepts success`() throws {
|
||||
try OpenClawSessionsCompactResponse.requireSuccess(
|
||||
from: Data(#"{"ok":true,"key":"agent:main:main","compacted":true}"#.utf8))
|
||||
}
|
||||
|
||||
@Test
|
||||
func compactResponseSurfacesGatewayFailureReason() {
|
||||
func `compact response surfaces gateway failure reason`() {
|
||||
let data = Data(
|
||||
#"{"ok":false,"key":"agent:main:main","compacted":false,"reason":"turn failed"}"#.utf8)
|
||||
do {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import OpenClawChatUI
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
@testable import OpenClawChatUI
|
||||
@testable import OpenClawKit
|
||||
|
||||
struct TalkWaveformMathTests {
|
||||
@Test
|
||||
|
||||
@@ -52,6 +52,8 @@ dispatch.
|
||||
| `test-performance-agent` | Separate workflow: daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
|
||||
| `openclaw-performance` | Separate workflow: daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.6 live lanes | Scheduled and manual dispatch |
|
||||
|
||||
Standalone Periphery workflows enforce zero dead-code findings for the iOS and macOS apps. The shared OpenClawKit workflow scans both consumers in parallel and reports a declaration only when Periphery emits the same Swift USR from both builds. Its generated `OpenClawProtocol/GatewayModels.swift` schema contract is retained as generator-owned code rather than treated as app-local dead code.
|
||||
|
||||
## Fail-fast order
|
||||
|
||||
1. `runner-admission` waits only for canonical `main` pushes; a newer push cancels the run before Blacksmith registration.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export type PeripheryFinding = {
|
||||
ids: string[];
|
||||
kind: string;
|
||||
location: string;
|
||||
name: string;
|
||||
hints?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type PeripheryIntersectionOptions = {
|
||||
iosResults: string;
|
||||
iosStatus: string;
|
||||
macosResults: string;
|
||||
macosStatus: string;
|
||||
output: string;
|
||||
};
|
||||
|
||||
export function parseArgs(args: string[]): PeripheryIntersectionOptions;
|
||||
export function validateFindings(value: unknown, label: string): PeripheryFinding[];
|
||||
export function intersectFindings(iosFindings: unknown, macosFindings: unknown): PeripheryFinding[];
|
||||
export function parseRepoLocation(location: string): {
|
||||
column: string;
|
||||
file: string;
|
||||
line: string;
|
||||
};
|
||||
export function escapeCommandData(value: unknown): string;
|
||||
export function escapeCommandProperty(value: unknown): string;
|
||||
export function formatAnnotation(finding: PeripheryFinding): string;
|
||||
export function buildSummary(findings: PeripheryFinding[]): string;
|
||||
export function run(args: string[], env?: NodeJS.ProcessEnv): number;
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
|
||||
const SHARED_LOCATION_PREFIX = "../shared/OpenClawKit/Sources/";
|
||||
|
||||
function requireValue(args, index, option) {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`${option} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseArgs(args) {
|
||||
const options = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const option = args[index];
|
||||
switch (option) {
|
||||
case "--ios-results":
|
||||
options.iosResults = requireValue(args, index, option);
|
||||
index += 1;
|
||||
break;
|
||||
case "--ios-status":
|
||||
options.iosStatus = requireValue(args, index, option);
|
||||
index += 1;
|
||||
break;
|
||||
case "--macos-results":
|
||||
options.macosResults = requireValue(args, index, option);
|
||||
index += 1;
|
||||
break;
|
||||
case "--macos-status":
|
||||
options.macosStatus = requireValue(args, index, option);
|
||||
index += 1;
|
||||
break;
|
||||
case "--output":
|
||||
options.output = requireValue(args, index, option);
|
||||
index += 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown option: ${option}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ["iosResults", "iosStatus", "macosResults", "macosStatus", "output"]) {
|
||||
if (!options[key]) {
|
||||
throw new Error(`missing required option: ${key}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function validateFindings(value, label) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`${label} results must be a JSON array`);
|
||||
}
|
||||
|
||||
return value.map((finding, index) => {
|
||||
if (!finding || typeof finding !== "object" || Array.isArray(finding)) {
|
||||
throw new Error(`${label} finding ${index} must be an object`);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(finding.ids) ||
|
||||
finding.ids.length === 0 ||
|
||||
finding.ids.some((id) => typeof id !== "string" || id.length === 0)
|
||||
) {
|
||||
throw new Error(`${label} finding ${index} has no usable Swift USR`);
|
||||
}
|
||||
if (
|
||||
typeof finding.location !== "string" ||
|
||||
!finding.location.startsWith(SHARED_LOCATION_PREFIX)
|
||||
) {
|
||||
throw new Error(`${label} finding ${index} is outside shared OpenClawKit sources`);
|
||||
}
|
||||
if (typeof finding.kind !== "string" || typeof finding.name !== "string") {
|
||||
throw new Error(`${label} finding ${index} is missing its kind or name`);
|
||||
}
|
||||
return finding;
|
||||
});
|
||||
}
|
||||
|
||||
export function intersectFindings(iosFindings, macosFindings) {
|
||||
const ios = validateFindings(iosFindings, "iOS");
|
||||
const macos = validateFindings(macosFindings, "macOS");
|
||||
const macosIds = new Set(macos.flatMap((finding) => finding.ids));
|
||||
|
||||
return ios
|
||||
.filter((finding) => finding.ids.some((id) => macosIds.has(id)))
|
||||
.toSorted((left, right) =>
|
||||
[left.location, left.kind, left.name]
|
||||
.join("\0")
|
||||
.localeCompare([right.location, right.kind, right.name].join("\0")),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseRepoLocation(location) {
|
||||
const match = /^(.*):(\d+):(\d+)$/.exec(location);
|
||||
if (!match || !match[1].startsWith("../shared/")) {
|
||||
throw new Error(`invalid shared Periphery location: ${location}`);
|
||||
}
|
||||
return {
|
||||
column: match[3],
|
||||
file: `apps/shared/${match[1].slice("../shared/".length)}`,
|
||||
line: match[2],
|
||||
};
|
||||
}
|
||||
|
||||
export function escapeCommandData(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("%", "%25")
|
||||
.replaceAll("\r", "%0D")
|
||||
.replaceAll("\n", "%0A");
|
||||
}
|
||||
|
||||
export function escapeCommandProperty(value) {
|
||||
return escapeCommandData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
||||
}
|
||||
|
||||
export function formatAnnotation(finding) {
|
||||
const location = parseRepoLocation(finding.location);
|
||||
const title = `${finding.kind || "Unused code"} ${finding.name}`.trim();
|
||||
return `::error file=${escapeCommandProperty(location.file)},line=${location.line},col=${location.column},title=Dead shared Swift code::${escapeCommandData(title)}`;
|
||||
}
|
||||
|
||||
export function buildSummary(findings) {
|
||||
if (findings.length === 0) {
|
||||
return [
|
||||
"### Shared OpenClawKit Periphery",
|
||||
"",
|
||||
"No declarations were reported dead by both the iOS and macOS consumer scans.",
|
||||
].join("\n");
|
||||
}
|
||||
return [
|
||||
"### Shared OpenClawKit Periphery",
|
||||
"",
|
||||
`Found ${findings.length} shared Swift ${findings.length === 1 ? "declaration" : "declarations"} reported dead by both consumer scans.`,
|
||||
"",
|
||||
"The gate matches Periphery's Swift USRs, not declaration names.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function readStatus(file, label) {
|
||||
const raw = fs.readFileSync(file, "utf8").trim();
|
||||
if (!/^\d+$/.test(raw)) {
|
||||
throw new Error(`${label} Periphery status is invalid`);
|
||||
}
|
||||
const status = Number(raw);
|
||||
if (status !== 0) {
|
||||
throw new Error(`${label} Periphery scan exited with status ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readFindings(file, label) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`${label} Periphery results are not valid JSON`, { cause: error });
|
||||
}
|
||||
return validateFindings(parsed, label);
|
||||
}
|
||||
|
||||
export function run(args, env = process.env) {
|
||||
const options = parseArgs(args);
|
||||
readStatus(options.iosStatus, "iOS");
|
||||
readStatus(options.macosStatus, "macOS");
|
||||
const findings = intersectFindings(
|
||||
readFindings(options.iosResults, "iOS"),
|
||||
readFindings(options.macosResults, "macOS"),
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(options.output), { recursive: true });
|
||||
fs.writeFileSync(options.output, `${JSON.stringify(findings, null, 2)}\n`);
|
||||
for (const finding of findings) {
|
||||
console.log(formatAnnotation(finding));
|
||||
}
|
||||
|
||||
const summary = buildSummary(findings);
|
||||
if (env.GITHUB_STEP_SUMMARY) {
|
||||
fs.appendFileSync(env.GITHUB_STEP_SUMMARY, `${summary}\n`);
|
||||
} else {
|
||||
console.log(summary);
|
||||
}
|
||||
return findings.length === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
||||
try {
|
||||
process.exitCode = run(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`::error title=Shared Periphery intersection failed::${escapeCommandData(message)}`,
|
||||
);
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
@@ -503,6 +503,10 @@ const GITHUB_WORKFLOW_OWNER_TEST_TARGETS = new Map([
|
||||
["test/scripts/ios-periphery-comment-workflow.test.ts"],
|
||||
],
|
||||
[".github/workflows/ios-periphery.yml", ["test/scripts/ios-periphery-comment-workflow.test.ts"]],
|
||||
[
|
||||
".github/workflows/shared-openclawkit-periphery.yml",
|
||||
["test/scripts/periphery-intersection.test.ts"],
|
||||
],
|
||||
[
|
||||
".github/workflows/live-media-runner-image.yml",
|
||||
["test/scripts/package-acceptance-workflow.test.ts"],
|
||||
@@ -789,6 +793,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
],
|
||||
],
|
||||
["scripts/ci-changed-scope.mjs", ["src/scripts/ci-changed-scope.test.ts"]],
|
||||
["scripts/periphery-intersection.mjs", ["test/scripts/periphery-intersection.test.ts"]],
|
||||
["scripts/ci-docker-pull-retry.sh", ["test/scripts/ci-docker-pull-retry.test.ts"]],
|
||||
["scripts/control-ui-i18n.ts", ["test/scripts/control-ui-i18n.test.ts"]],
|
||||
["scripts/apple-app-i18n.ts", ["test/scripts/apple-app-i18n.test.ts"]],
|
||||
@@ -2060,6 +2065,7 @@ const TOOLING_DECLARATION_SOURCE_MIRRORS = [
|
||||
["scripts/ci-changed-scope.d.mts", "scripts/ci-changed-scope.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/periphery-intersection.d.mts", "scripts/periphery-intersection.mjs"],
|
||||
[
|
||||
"scripts/lib/bundled-plugin-build-entries.d.mts",
|
||||
"scripts/lib/bundled-plugin-build-entries.mjs",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { compileFunction } from "node:vm";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import {
|
||||
buildSummary,
|
||||
formatAnnotation,
|
||||
intersectFindings,
|
||||
parseRepoLocation,
|
||||
validateFindings,
|
||||
} from "../../scripts/periphery-intersection.mjs";
|
||||
|
||||
const WORKFLOW_PATH = ".github/workflows/shared-openclawkit-periphery.yml";
|
||||
|
||||
type WorkflowStep = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
run?: string;
|
||||
with?: { name?: string; path?: string; script?: string };
|
||||
};
|
||||
|
||||
type Workflow = {
|
||||
jobs?: Record<
|
||||
string,
|
||||
{
|
||||
name?: string;
|
||||
needs?: string[] | string;
|
||||
steps?: WorkflowStep[];
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
function finding(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
ids: ["s:11OpenClawKit7ExampleV"],
|
||||
kind: "struct",
|
||||
location: "../shared/OpenClawKit/Sources/OpenClawKit/Example.swift:12:8",
|
||||
name: "Example",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Periphery intersection", () => {
|
||||
it("matches exact Swift USRs instead of declaration names", () => {
|
||||
const sameNameDifferentUsr = finding({ ids: ["s:11OpenClawKit7ExampleV_other"] });
|
||||
expect(intersectFindings([finding()], [sameNameDifferentUsr])).toEqual([]);
|
||||
expect(intersectFindings([finding()], [finding()])).toEqual([finding()]);
|
||||
});
|
||||
|
||||
it("matches any USR emitted for a declaration compiled into multiple iOS modules", () => {
|
||||
const ios = finding({ ids: ["s:16OpenClawWatchApp7ExampleV", "s:11OpenClawKit7ExampleV"] });
|
||||
expect(intersectFindings([ios], [finding()])).toEqual([ios]);
|
||||
});
|
||||
|
||||
it("sorts findings deterministically", () => {
|
||||
const later = finding({
|
||||
ids: ["s:11OpenClawKit5LaterV"],
|
||||
location: "../shared/OpenClawKit/Sources/OpenClawKit/Later.swift:2:1",
|
||||
name: "Later",
|
||||
});
|
||||
expect(intersectFindings([later, finding()], [finding(), later])).toEqual([finding(), later]);
|
||||
});
|
||||
|
||||
it("fails closed when a finding has no USR", () => {
|
||||
expect(() => validateFindings([finding({ ids: [] })], "iOS")).toThrow(
|
||||
"iOS finding 0 has no usable Swift USR",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects findings outside shared OpenClawKit", () => {
|
||||
expect(() =>
|
||||
validateFindings([finding({ location: "Sources/App.swift:1:1" })], "macOS"),
|
||||
).toThrow("macOS finding 0 is outside shared OpenClawKit sources");
|
||||
});
|
||||
|
||||
it("maps relative scan locations to repository annotations", () => {
|
||||
expect(parseRepoLocation(finding().location)).toEqual({
|
||||
column: "8",
|
||||
file: "apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift",
|
||||
line: "12",
|
||||
});
|
||||
expect(formatAnnotation(finding())).toBe(
|
||||
"::error file=apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift,line=12,col=8,title=Dead shared Swift code::struct Example",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports the zero-findings policy in the summary", () => {
|
||||
expect(buildSummary([])).toContain("No declarations were reported dead by both");
|
||||
expect(buildSummary([finding()])).toContain("Found 1 shared Swift declaration");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared OpenClawKit Periphery workflow", () => {
|
||||
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as Workflow;
|
||||
|
||||
it("runs two consumer scans and a same-run intersection", () => {
|
||||
expect(workflow.jobs?.["scan-ios"]?.name).toBe("Scan shared kit from iOS");
|
||||
expect(workflow.jobs?.["scan-macos"]?.name).toBe("Scan shared kit from macOS");
|
||||
expect(workflow.jobs?.intersect?.needs).toEqual(["scope", "scan-ios", "scan-macos"]);
|
||||
|
||||
const iosUpload = workflow.jobs?.["scan-ios"]?.steps?.find(
|
||||
(step) => step.name === "Upload iOS consumer report",
|
||||
);
|
||||
const macosUpload = workflow.jobs?.["scan-macos"]?.steps?.find(
|
||||
(step) => step.name === "Upload macOS consumer report",
|
||||
);
|
||||
expect(iosUpload?.with?.name).toContain("shared-periphery-ios-");
|
||||
expect(macosUpload?.with?.name).toContain("shared-periphery-macos-");
|
||||
});
|
||||
|
||||
it("retains the generated protocol contract and leaves findings for the intersection", () => {
|
||||
for (const jobName of ["scan-ios", "scan-macos"]) {
|
||||
const scan = workflow.jobs?.[jobName]?.steps?.find((step) => step.name === "Scan shared kit");
|
||||
expect(scan?.run).toContain("--report-include '../shared/OpenClawKit/Sources/**'");
|
||||
expect(scan?.run).toContain(
|
||||
"--retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift'",
|
||||
);
|
||||
expect(scan?.run).not.toContain("--strict");
|
||||
}
|
||||
const macosScan = workflow.jobs?.["scan-macos"]?.steps?.find(
|
||||
(step) => step.name === "Scan shared kit",
|
||||
);
|
||||
expect(macosScan?.run).not.toContain("--exclude-tests");
|
||||
});
|
||||
|
||||
it("scopes native consumer, shared package, and workflow changes", async () => {
|
||||
const script = workflow.jobs?.scope?.steps?.find((step) => step.id === "scope")?.with?.script;
|
||||
expect(script).toBeTruthy();
|
||||
const execute = compileFunction(`return (async () => {\n${script}\n})();`, [
|
||||
"context",
|
||||
"core",
|
||||
"github",
|
||||
]) as (context: unknown, core: unknown, github: unknown) => Promise<void>;
|
||||
|
||||
for (const filename of [
|
||||
"apps/ios/Sources/App.swift",
|
||||
"apps/macos/Sources/OpenClaw/App.swift",
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift",
|
||||
WORKFLOW_PATH,
|
||||
]) {
|
||||
const outputs = new Map<string, string>();
|
||||
await execute(
|
||||
{
|
||||
eventName: "pull_request",
|
||||
payload: { pull_request: { draft: false, number: 1 } },
|
||||
repo: {},
|
||||
},
|
||||
{ setOutput: (name: string, value: string) => outputs.set(name, value) },
|
||||
{
|
||||
paginate: async () => [{ filename }],
|
||||
rest: { pulls: { listFiles() {} } },
|
||||
},
|
||||
);
|
||||
expect(outputs.get("should-scan"), filename).toBe("true");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user