Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Editor/editorfe/src/scriptWidgetAPI.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Scripts/release-readiness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ if [ "${SCRIPTWIDGET_SKIP_XCODE:-0}" != "1" ]; then
node Scripts/test-runtime-delivery.mjs
node Scripts/test-service-authorization.mjs
node Scripts/test-storage-atomic.mjs
node Scripts/test-icloud-json.mjs
node Scripts/test-storage-files.mjs
sh Scripts/macos-widget-link-tests.sh
xcodebuild -quiet -project macOS/ScriptWidgetMac.xcodeproj -scheme ScriptWidgetRuntimeTests \
Expand Down
45 changes: 45 additions & 0 deletions Scripts/test-icloud-json.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Run the actual CloudJSON XCTest cases without an app/testmanagerd session.
// Compile unchanged production file helpers and storage services; all fixtures
// are temporary directories. This supplements, never replaces, app-host tests.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const exec = promisify(execFile);
const repo = fileURLToPath(new URL("..", import.meta.url));
const temporary = await mkdtemp(join(tmpdir(), "scriptwidget-cloud-json-"));
try {
const developer = (await exec("xcode-select", ["-p"])).stdout.trim();
const platform = join(developer, "Platforms/MacOSX.platform/Developer");
const frameworks = join(platform, "Library/Frameworks"), libraries = join(platform, "usr/lib");
const source = await readFile(join(repo, "Shared/ScriptWidgetRuntime/Common/ScriptWidgetPackage.swift"), "utf8");
const start = source.indexOf("enum ScriptWidgetMetadataFile {"), end = source.indexOf("/// Keeps private execution resources");
if (start < 0 || end <= start) throw new Error("Production file-helper boundaries changed; update the probe.");
await writeFile(join(temporary, "helpers.swift"), "import Foundation\nimport Darwin\n" + source.slice(start, end));
const tests = await readFile(join(repo, "Tests/ScriptWidgetRuntimeTests/PackageCacheTests.swift"), "utf8");
const testStart = tests.indexOf("final class CloudJSONTests: XCTestCase {");
if (testStart < 0) throw new Error("Missing CloudJSON fixture tests.");
await writeFile(join(temporary, "main.swift"), "import XCTest\n" + tests.slice(testStart) + `
let suite = CloudJSONTests.defaultTestSuite
suite.run()
guard let result = suite.testRun, result.executionCount >= 10, result.totalFailureCount == 0 else { exit(1) }
`);
const binary = join(temporary, "tests");
await exec("xcrun", ["swiftc", "-F", frameworks, "-I", libraries, "-L", libraries,
...[frameworks, libraries, join(platform, "Library/PrivateFrameworks"), resolve(developer, "../SharedFrameworks")]
.flatMap(path => ["-Xlinker", "-rpath", "-Xlinker", path]),
...["Shared/ScriptWidgetRuntime/Widget/API/ScriptWidgetStorageLock.swift",
"Shared/ScriptWidgetRuntime/Widget/API/ScriptWidgetStorageNamespaces.swift",
"Shared/ScriptWidgetRuntime/Common/ScriptWidgetCloudJSON.swift"].map(path => resolve(repo, path)),
join(temporary, "helpers.swift"), join(temporary, "main.swift"), "-o", binary], { timeout: 60000 });
const result = await exec(binary, [], { timeout: 30000 });
process.stdout.write(result.stdout + result.stderr);
} catch (error) {
process.stderr.write(error.stdout ?? "");
process.stderr.write(error.stderr ?? "");
throw error;
} finally {
await rm(temporary, { recursive: true, force: true });
}
7 changes: 7 additions & 0 deletions Shared/ScriptWidgetRuntime/AI/PromptBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ enum PromptBuilder {
`$storage.getString(key)` for strings; `$storage.setJSON(key, object)`
and `$storage.getJSON(key)` for objects. Check write success.
Do not invent `$storage.set` or `$storage.get`.
`$storage` stays local. Opt-in `$icloud` requires permission `icloud`,
minimumRuntimeVersion 1.5 and an explicit native app connection.
readJSON(name) returns {value,revision,state,conflicts}; user actions
can writeJSON(name,value,revision) or explicitly resolveJSON after
reviewing every head. Never claim a local save is already uploaded,
silently migrate existing records or choose a store ID in scripts.
Browser previews use isolated mocks, not real iCloud.
7. Only use tags, props, and APIs that appear in the REFERENCE
section below. Do not invent new ones.
8. When calling `fetch`, always wrap it in try/catch so the
Expand Down
4 changes: 4 additions & 0 deletions Shared/ScriptWidgetRuntime/Common/ScriptMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ enum WidgetPackagePermission: String, Codable, CaseIterable, Identifiable {
case services
case music
case storage
case icloud
case files

var id: String { rawValue }
Expand Down Expand Up @@ -305,6 +306,9 @@ enum WidgetPackageManifestValidator {
error("unsupported_minimum_runtime", "This package requires a newer or incompatible ScriptWidget Runtime. Update the app before running it.")
}
}
if manifest.permissions.contains(.icloud), !WidgetRuntimeRequirement.supports("1.5", current: manifest.minimumRuntimeVersion ?? "0.0") {
error("icloud_runtime", "iCloud JSON requires minimumRuntimeVersion 1.5 or later.")
}
if let timeline = manifest.timeline, !timeline.isValid {
error("invalid_timeline", "timeline requires 1–4 sorted unique dailyTimes in HH:mm and a valid timeZone.")
}
Expand Down
Loading
Loading