mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
535 lines
23 KiB
Swift
535 lines
23 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
struct MacNodeClaudeSessionCatalogTests {
|
|
private final class EnumerationCounter: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var count = 0
|
|
|
|
func increment() {
|
|
self.lock.withLock { self.count += 1 }
|
|
}
|
|
|
|
func value() -> Int {
|
|
self.lock.withLock { self.count }
|
|
}
|
|
}
|
|
|
|
private final class Fixture {
|
|
let home: URL
|
|
|
|
init() throws {
|
|
self.home = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-claude-home-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: self.home, withIntermediateDirectories: true)
|
|
}
|
|
|
|
deinit {
|
|
MacNodeClaudeSessionCatalog.setCatalogEnumerationObserverForTesting(nil)
|
|
try? FileManager.default.removeItem(at: self.home)
|
|
}
|
|
|
|
var projects: URL {
|
|
self.home.appendingPathComponent(".claude/projects", isDirectory: true)
|
|
}
|
|
|
|
func project(_ name: String = "-workspace") throws -> URL {
|
|
let url = self.projects.appendingPathComponent(name, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
func file(_ sessionId: String, in project: URL) -> URL {
|
|
project.appendingPathComponent("\(sessionId).jsonl")
|
|
}
|
|
|
|
func indexEntry(
|
|
_ sessionId: String,
|
|
in project: URL,
|
|
summary: String? = nil,
|
|
modified: String? = nil,
|
|
projectPath: String? = nil,
|
|
isSidechain: Bool = false) -> [String: Any]
|
|
{
|
|
var entry: [String: Any] = [
|
|
"sessionId": sessionId,
|
|
"fullPath": self.file(sessionId, in: project).path,
|
|
"isSidechain": isSidechain,
|
|
]
|
|
entry["summary"] = summary
|
|
entry["modified"] = modified
|
|
entry["projectPath"] = projectPath
|
|
return entry
|
|
}
|
|
|
|
func writeIndex(_ entries: [[String: Any]], in project: URL) throws {
|
|
try self.writeJSON(
|
|
["version": 1, "entries": entries],
|
|
to: project.appendingPathComponent("sessions-index.json"))
|
|
}
|
|
|
|
func desktopMetadata(_ name: String) -> URL {
|
|
self.home.appendingPathComponent(
|
|
"Library/Application Support/Claude/claude-code-sessions/account/workspace/\(name).json")
|
|
}
|
|
|
|
func writeJSON(_ value: Any, to url: URL) throws {
|
|
try FileManager.default.createDirectory(
|
|
at: url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]).write(to: url)
|
|
}
|
|
|
|
func message(
|
|
_ sessionId: String,
|
|
_ text: String,
|
|
role: String = "user",
|
|
index: Int = 1,
|
|
entrypoint: String? = nil,
|
|
cwd: String? = nil,
|
|
version: String? = nil,
|
|
isSidechain: Bool = false) -> [String: Any]
|
|
{
|
|
var row: [String: Any] = [
|
|
"type": role,
|
|
"sessionId": sessionId,
|
|
"uuid": "\(sessionId)-\(index)",
|
|
"timestamp": "2026-07-0\(index)T00:00:00.000Z",
|
|
"isSidechain": isSidechain,
|
|
"message": [
|
|
"role": role,
|
|
"content": [["type": "text", "text": text]],
|
|
"model": "claude-opus-4-8",
|
|
],
|
|
]
|
|
row["entrypoint"] = entrypoint
|
|
row["cwd"] = cwd
|
|
row["version"] = version
|
|
return row
|
|
}
|
|
|
|
func writeTranscript(_ rows: [[String: Any]], to url: URL) throws {
|
|
let lines = try rows.map { row in
|
|
try #require(String(
|
|
data: JSONSerialization.data(withJSONObject: row, options: [.sortedKeys]),
|
|
encoding: .utf8))
|
|
}
|
|
try (lines.joined(separator: "\n") + "\n").write(
|
|
to: url,
|
|
atomically: true,
|
|
encoding: .utf8)
|
|
}
|
|
|
|
func params(_ value: [String: Any]) throws -> String? {
|
|
try String(data: JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]), encoding: .utf8)
|
|
}
|
|
|
|
func decode(_ json: String) -> [String: Any]? {
|
|
try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any]
|
|
}
|
|
|
|
func list(paramsJSON: String? = nil) throws -> [String: Any]? {
|
|
try self.decode(MacNodeClaudeSessionCatalog.list(paramsJSON: paramsJSON, homeURL: self.home))
|
|
}
|
|
|
|
func read(_ paramsJSON: String) throws -> [String: Any]? {
|
|
try self.decode(self.readJSON(paramsJSON))
|
|
}
|
|
|
|
func readJSON(_ paramsJSON: String) throws -> String {
|
|
try MacNodeClaudeSessionCatalog.read(paramsJSON: paramsJSON, homeURL: self.home)
|
|
}
|
|
|
|
func observeEnumerations() -> EnumerationCounter {
|
|
let counter = EnumerationCounter()
|
|
let projectsPath = self.projects.standardizedFileURL.path
|
|
MacNodeClaudeSessionCatalog.setCatalogEnumerationObserverForTesting { rootPath in
|
|
if rootPath == projectsPath { counter.increment() }
|
|
}
|
|
return counter
|
|
}
|
|
}
|
|
|
|
@Test func `merges Desktop metadata over CLI indexes and filters archived sessions`() throws {
|
|
let fixture = try Fixture()
|
|
let desktopId = "desktop-session"
|
|
let cliId = "cli-session"
|
|
let archivedId = "archived-session"
|
|
let project = try fixture.project()
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(
|
|
cliId,
|
|
in: project,
|
|
summary: "CLI title",
|
|
modified: "2026-07-01T00:00:00.000Z",
|
|
projectPath: "/work/cli"),
|
|
fixture.indexEntry(
|
|
desktopId,
|
|
in: project,
|
|
summary: "Index title",
|
|
modified: "2026-07-02T00:00:00.000Z"),
|
|
fixture.indexEntry(
|
|
archivedId,
|
|
in: project,
|
|
summary: "Archived",
|
|
modified: "2026-07-03T00:00:00.000Z"),
|
|
], in: project)
|
|
for sessionId in [desktopId, cliId, archivedId] {
|
|
try fixture.writeTranscript(
|
|
[fixture.message(sessionId, sessionId)],
|
|
to: fixture.file(sessionId, in: project))
|
|
}
|
|
try fixture.writeJSON(
|
|
[
|
|
"sessionId": "local-active",
|
|
"cliSessionId": desktopId,
|
|
"title": "Desktop title",
|
|
"cwd": "/desktop/cwd",
|
|
"lastActivityAt": 1_783_137_600_000 as Int64,
|
|
"isArchived": false,
|
|
],
|
|
to: fixture.desktopMetadata("local_active"))
|
|
try fixture.writeJSON(
|
|
[
|
|
"sessionId": "local-archived",
|
|
"cliSessionId": archivedId,
|
|
"isArchived": true,
|
|
],
|
|
to: fixture.desktopMetadata("local_archived"))
|
|
|
|
let first = try #require(try fixture.list(paramsJSON: #"{"limit":1}"#))
|
|
let firstSessions = try #require(first["sessions"] as? [[String: Any]])
|
|
#expect(firstSessions.count == 1)
|
|
#expect(firstSessions[0]["threadId"] as? String == desktopId)
|
|
#expect(firstSessions[0]["name"] as? String == "Desktop title")
|
|
#expect(firstSessions[0]["source"] as? String == "claude-desktop")
|
|
#expect(firstSessions[0]["cwd"] as? String == "/desktop/cwd")
|
|
let cursor = try #require(first["nextCursor"] as? String)
|
|
|
|
let secondParams = try fixture.params(["limit": 1, "cursor": cursor])
|
|
let second = try #require(try fixture.list(paramsJSON: secondParams))
|
|
let secondSessions = try #require(second["sessions"] as? [[String: Any]])
|
|
#expect(secondSessions.map { $0["threadId"] as? String } == [cliId])
|
|
#expect(secondSessions[0]["updatedAt"] as? Int64 == 1_782_864_000_000)
|
|
#expect(second["nextCursor"] == nil)
|
|
#expect(throws: MacNodeClaudeSessionCatalog.CatalogError.self) {
|
|
try fixture.read(#"{"threadId":"archived-session","limit":1}"#)
|
|
}
|
|
}
|
|
|
|
@Test func `discovers CLI fallback transcripts and rejects sidechains foreign entrypoints and escapes`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sidechainId = "sidechain-session"
|
|
let cliSidechainId = "cli-sidechain-session"
|
|
let discoveredSidechainId = "discovered-sidechain"
|
|
let foreignEntrypointId = "foreign-entrypoint-session"
|
|
let unindexedId = "unindexed-session"
|
|
let cliId = "cli-session"
|
|
let sdkCLIId = "sdk-cli-session"
|
|
let escapedId = "escaped-session"
|
|
let escapedURL = fixture.file(escapedId, in: project)
|
|
let outsideURL = fixture.home.appendingPathComponent("outside.jsonl")
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(sidechainId, in: project, isSidechain: true),
|
|
fixture.indexEntry(escapedId, in: project),
|
|
], in: project)
|
|
try fixture.writeTranscript(
|
|
[fixture.message(sidechainId, "sidechain")],
|
|
to: fixture.file(sidechainId, in: project))
|
|
try fixture.writeTranscript(
|
|
[fixture.message(unindexedId, "unindexed")],
|
|
to: fixture.file(unindexedId, in: project))
|
|
for (id, text, entrypoint, cwd, version) in [
|
|
(cliId, "Interactive CLI prompt", "cli", "/work/cli", "2.1.216"),
|
|
(sdkCLIId, "Headless CLI prompt", "sdk-cli", "/work/sdk", "2.1.204"),
|
|
] {
|
|
try fixture.writeTranscript(
|
|
[fixture.message(
|
|
id,
|
|
text,
|
|
entrypoint: entrypoint,
|
|
cwd: cwd,
|
|
version: version)],
|
|
to: fixture.file(id, in: project))
|
|
}
|
|
for (id, text, entrypoint, isSidechain) in [
|
|
(cliSidechainId, "interactive sidechain", "cli", true),
|
|
(discoveredSidechainId, "headless sidechain", "sdk-cli", true),
|
|
(foreignEntrypointId, "SDK session", "sdk-ts", false),
|
|
] {
|
|
try fixture.writeTranscript(
|
|
[fixture.message(
|
|
id,
|
|
text,
|
|
entrypoint: entrypoint,
|
|
isSidechain: isSidechain)],
|
|
to: fixture.file(id, in: project))
|
|
}
|
|
try fixture.writeTranscript(
|
|
[fixture.message(escapedId, "outside")],
|
|
to: outsideURL)
|
|
try FileManager.default.createSymbolicLink(at: escapedURL, withDestinationURL: outsideURL)
|
|
for (name, sessionId, title) in [
|
|
("local_sidechain", sidechainId, "Desktop sidechain"),
|
|
("local_cli_sidechain", cliSidechainId, "Interactive Desktop sidechain"),
|
|
("local_discovered_sidechain", discoveredSidechainId, "Headless Desktop sidechain"),
|
|
] {
|
|
try fixture.writeJSON(
|
|
["cliSessionId": sessionId, "title": title, "isArchived": false],
|
|
to: fixture.desktopMetadata(name))
|
|
}
|
|
|
|
let list = try #require(try fixture.list())
|
|
let sessions = try #require(list["sessions"] as? [[String: Any]])
|
|
#expect(sessions.compactMap { $0["threadId"] as? String }.sorted() == [cliId, sdkCLIId])
|
|
#expect(
|
|
sessions.first(where: { $0["threadId"] as? String == cliId })?["name"] as? String ==
|
|
"Interactive CLI prompt")
|
|
for (threadId, expectedText) in [
|
|
(cliId, "Interactive CLI prompt"),
|
|
(sdkCLIId, "Headless CLI prompt"),
|
|
] {
|
|
let read = try #require(try fixture.read(#"{"threadId":"\#(threadId)","limit":1}"#))
|
|
let items = try #require(read["items"] as? [[String: Any]])
|
|
#expect(items.first?["text"] as? String == expectedText)
|
|
}
|
|
for threadId in [
|
|
sidechainId,
|
|
cliSidechainId,
|
|
discoveredSidechainId,
|
|
foreignEntrypointId,
|
|
unindexedId,
|
|
escapedId,
|
|
] {
|
|
#expect(throws: MacNodeClaudeSessionCatalog.CatalogError.self) {
|
|
try fixture.read(#"{"threadId":"\#(threadId)","limit":1}"#)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func `cached metadata honors access revocation and refreshes replaced transcripts`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sessionId = "cached-session"
|
|
let transcript = fixture.file(sessionId, in: project)
|
|
try fixture.writeTranscript(
|
|
[fixture.message(
|
|
sessionId,
|
|
"Alpha",
|
|
entrypoint: "sdk-cli")],
|
|
to: transcript)
|
|
|
|
let first = try #require(try fixture.list())
|
|
#expect((first["sessions"] as? [[String: Any]])?.first?["name"] as? String == "Alpha")
|
|
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: transcript.path)
|
|
defer {
|
|
try? FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o600],
|
|
ofItemAtPath: transcript.path)
|
|
}
|
|
let cached = try #require(try fixture.list())
|
|
#expect((cached["sessions"] as? [[String: Any]])?.isEmpty == true)
|
|
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: transcript.path)
|
|
let restored = try #require(try fixture.list())
|
|
#expect((restored["sessions"] as? [[String: Any]])?.first?["name"] as? String == "Alpha")
|
|
|
|
try fixture.writeTranscript(
|
|
[fixture.message(
|
|
sessionId,
|
|
"Bravo",
|
|
index: 2,
|
|
entrypoint: "sdk-cli")],
|
|
to: transcript)
|
|
let refreshed = try #require(try fixture.list())
|
|
#expect((refreshed["sessions"] as? [[String: Any]])?.first?["name"] as? String == "Bravo")
|
|
}
|
|
|
|
@Test func `reuses catalog discovery only across transcript read pages`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sessionId = "transcript-session"
|
|
let oldUser = String(repeating: "old user ", count: 20000)
|
|
let transcript = fixture.file(sessionId, in: project)
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(
|
|
sessionId,
|
|
in: project,
|
|
summary: "Transcript",
|
|
modified: "2026-07-04T00:00:00.000Z"),
|
|
], in: project)
|
|
try fixture.writeTranscript([
|
|
["type": "queue-operation", "sessionId": sessionId],
|
|
fixture.message(sessionId, oldUser),
|
|
fixture.message(sessionId, "old assistant", role: "assistant", index: 2),
|
|
fixture.message(sessionId, "new user", index: 3),
|
|
fixture.message(sessionId, "new assistant", role: "assistant", index: 4),
|
|
], to: transcript)
|
|
|
|
let enumerations = fixture.observeEnumerations()
|
|
|
|
let latest = try #require(try fixture.read(#"{"threadId":"transcript-session","limit":2}"#))
|
|
let latestItems = try #require(latest["items"] as? [[String: Any]])
|
|
#expect(latestItems.map { $0["text"] as? String } == ["new assistant", "new user"])
|
|
let cursor = try #require(latest["nextCursor"] as? String)
|
|
#expect(enumerations.value() == 1)
|
|
|
|
let olderParams = try #require(try fixture.params([
|
|
"threadId": sessionId,
|
|
"limit": 2,
|
|
"cursor": cursor,
|
|
]))
|
|
let older = try #require(try fixture.read(olderParams))
|
|
let olderItems = try #require(older["items"] as? [[String: Any]])
|
|
#expect(olderItems.map { $0["text"] as? String } == ["old assistant", oldUser])
|
|
#expect(older["nextCursor"] == nil)
|
|
#expect(enumerations.value() == 1)
|
|
}
|
|
|
|
@Test func `missing pagination lease falls back to current discovery`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sessionId = "lease-session"
|
|
let transcript = fixture.file(sessionId, in: project)
|
|
try fixture.writeIndex([fixture.indexEntry(sessionId, in: project)], in: project)
|
|
try fixture.writeTranscript([
|
|
fixture.message(sessionId, "old"),
|
|
fixture.message(sessionId, "new", role: "assistant", index: 2),
|
|
], to: transcript)
|
|
|
|
let enumerations = fixture.observeEnumerations()
|
|
|
|
let first = try #require(try fixture.read(#"{"threadId":"lease-session","limit":1}"#))
|
|
var encoded = try #require(first["nextCursor"] as? String)
|
|
.replacingOccurrences(of: "-", with: "+")
|
|
.replacingOccurrences(of: "_", with: "/")
|
|
encoded += String(repeating: "=", count: (4 - encoded.count % 4) % 4)
|
|
let cursorData = try #require(Data(base64Encoded: encoded))
|
|
var cursorValue = try #require(
|
|
JSONSerialization.jsonObject(with: cursorData) as? [String: Any])
|
|
cursorValue["lease"] = UUID().uuidString
|
|
let missingLeaseCursor = try JSONSerialization.data(
|
|
withJSONObject: cursorValue,
|
|
options: [.sortedKeys]).base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
.replacingOccurrences(of: "=", with: "")
|
|
|
|
let secondParams = try #require(try fixture.params([
|
|
"threadId": sessionId,
|
|
"limit": 1,
|
|
"cursor": missingLeaseCursor,
|
|
]))
|
|
let second = try #require(try fixture.read(secondParams))
|
|
#expect((second["items"] as? [[String: Any]])?.first?["text"] as? String == "old")
|
|
#expect(enumerations.value() == 2)
|
|
}
|
|
|
|
@Test func `fresh reads recheck catalog eligibility after a pagination lease`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sessionId = "eligibility-session"
|
|
try fixture.writeTranscript([
|
|
fixture.message(sessionId, "old"),
|
|
fixture.message(sessionId, "new", role: "assistant", index: 2),
|
|
], to: fixture.file(sessionId, in: project))
|
|
try fixture.writeIndex([fixture.indexEntry(sessionId, in: project)], in: project)
|
|
|
|
let first = try #require(try fixture.read(#"{"threadId":"eligibility-session","limit":1}"#))
|
|
#expect(first["nextCursor"] is String)
|
|
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(sessionId, in: project, isSidechain: true),
|
|
], in: project)
|
|
|
|
#expect(throws: MacNodeClaudeSessionCatalog.CatalogError.self) {
|
|
try fixture.read(#"{"threadId":"eligibility-session","limit":1}"#)
|
|
}
|
|
}
|
|
|
|
@Test func `fresh reads discover a moved session file`() throws {
|
|
let fixture = try Fixture()
|
|
let firstProject = try fixture.project("-first")
|
|
let sessionId = "moving-session"
|
|
let firstTranscript = fixture.file(sessionId, in: firstProject)
|
|
try fixture.writeTranscript([
|
|
fixture.message(sessionId, "first location"),
|
|
fixture.message(sessionId, "latest", role: "assistant", index: 2),
|
|
], to: firstTranscript)
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(sessionId, in: firstProject, summary: "Moving"),
|
|
], in: firstProject)
|
|
|
|
let enumerations = fixture.observeEnumerations()
|
|
|
|
let firstPayload = try #require(try fixture.read(#"{"threadId":"moving-session","limit":1}"#))
|
|
#expect((firstPayload["items"] as? [[String: Any]])?.first?["text"] as? String == "latest")
|
|
#expect(enumerations.value() == 1)
|
|
|
|
let secondProject = try fixture.project("-second")
|
|
let secondTranscript = fixture.file(sessionId, in: secondProject)
|
|
try FileManager.default.moveItem(at: firstTranscript, to: secondTranscript)
|
|
try fixture.writeIndex([], in: firstProject)
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(sessionId, in: secondProject, summary: "Moving"),
|
|
], in: secondProject)
|
|
|
|
let refreshedPayload = try #require(try fixture.read(#"{"threadId":"moving-session","limit":1}"#))
|
|
#expect((refreshedPayload["items"] as? [[String: Any]])?.first?["text"] as? String == "latest")
|
|
#expect(enumerations.value() == 2)
|
|
|
|
_ = try fixture.read(#"{"threadId":"moving-session","limit":1}"#)
|
|
#expect(enumerations.value() == 3)
|
|
}
|
|
|
|
@Test func `bounds oversized transcript items by encoded response size`() throws {
|
|
let fixture = try Fixture()
|
|
let project = try fixture.project()
|
|
let sessionId = "oversized-session"
|
|
let transcript = fixture.file(sessionId, in: project)
|
|
try fixture.writeIndex([
|
|
fixture.indexEntry(
|
|
sessionId,
|
|
in: project,
|
|
summary: "Oversized",
|
|
modified: "2026-07-04T00:00:00.000Z"),
|
|
], in: project)
|
|
let oneGrapheme = "a" + String(repeating: "\u{0301}", count: 2_200_000)
|
|
try fixture.writeTranscript(
|
|
[fixture.message(sessionId, oneGrapheme)],
|
|
to: transcript)
|
|
|
|
let response = try fixture.readJSON(#"{"threadId":"oversized-session","limit":1}"#)
|
|
#expect(response.utf8.count <= 20 * 1024 * 1024)
|
|
let decoded = try #require(fixture.decode(response))
|
|
let items = try #require(decoded["items"] as? [[String: Any]])
|
|
#expect(items.first?["truncated"] as? Bool == true)
|
|
let itemData = try JSONSerialization.data(withJSONObject: #require(items.first))
|
|
#expect(itemData.count <= 4 * 1024 * 1024)
|
|
}
|
|
|
|
@Test func `advertises only when Anthropic is enabled and Claude projects exist`() throws {
|
|
let fixture = try Fixture()
|
|
let root: [String: Any] = ["plugins": ["entries": ["anthropic": ["enabled": true]]]]
|
|
#expect(!MacNodeClaudeSessionCatalog.shouldAdvertise(root: root, homeURL: fixture.home))
|
|
try FileManager.default.createDirectory(
|
|
at: fixture.projects,
|
|
withIntermediateDirectories: true)
|
|
#expect(MacNodeClaudeSessionCatalog.shouldAdvertise(root: [:], homeURL: fixture.home))
|
|
#expect(MacNodeClaudeSessionCatalog.shouldAdvertise(root: root, homeURL: fixture.home))
|
|
let disabled: [String: Any] = ["plugins": ["entries": ["anthropic": ["enabled": false]]]]
|
|
#expect(!MacNodeClaudeSessionCatalog.shouldAdvertise(root: disabled, homeURL: fixture.home))
|
|
let denied: [String: Any] = ["plugins": ["deny": ["anthropic"]]]
|
|
#expect(!MacNodeClaudeSessionCatalog.shouldAdvertise(root: denied, homeURL: fixture.home))
|
|
let omittedByAllowlist: [String: Any] = ["plugins": ["allow": ["codex"]]]
|
|
#expect(!MacNodeClaudeSessionCatalog.shouldAdvertise(root: omittedByAllowlist, homeURL: fixture.home))
|
|
let entries: [String: Any] = ["anthropic": ["enabled": true], "Anthropic": ["enabled": false]]
|
|
let ambiguous: [String: Any] = ["plugins": ["entries": entries]]
|
|
#expect(!MacNodeClaudeSessionCatalog.shouldAdvertise(root: ambiguous, homeURL: fixture.home))
|
|
}
|
|
}
|