Files
agent-pet-companion/apps/macos/Tests/AgentPetCompanionCoreTests/GenerationSessionStateTests.swift
T
2026-08-09 17:18:27 +08:00

701 lines
28 KiB
Swift

import Foundation
import Testing
@testable import AgentPetCompanionCore
@Suite
struct GenerationSessionStateTests {
@Test("generation forms contain only user-authored maker inputs")
func generationFormCodableContract() throws {
let form = GenerationForm(
description: "A small pixel companion",
style: "像素",
quality: .low,
referenceImages: ["/tmp/reference.png"]
)
let encoded = try JSONEncoder().encode(form)
let object = try #require(
JSONSerialization.jsonObject(with: encoded) as? [String: Any]
)
#expect(Set(object.keys) == [
"description",
"style",
"quality",
"reference_images",
])
#expect(try JSONDecoder().decode(GenerationForm.self, from: encoded) == form)
}
@Test("pet summaries require the complete nine-action timing contract")
func petSummaryRejectsMissingTimingContract() {
let json = #"{"id":"pet","name":"Pet","style":"像素","quality":"standard","render_size":{"width":384,"height":416},"petpack_path":"/tmp/pet.petpack","cover_path":"/tmp/cover.png","active":false,"created_at":"2026-07-31T00:00:00Z"}"#
#expect(throws: DecodingError.self) {
try JSONDecoder().decode(PetSummary.self, from: Data(json.utf8))
}
}
@Test("a pet without a private generation job decodes as empty history")
func missingGenerationHistoryDecodes() throws {
let data = Data(#"{"found":false,"ok":true,"pet_id":"pet_external"}"#.utf8)
let history = try JSONDecoder().decode(GenerationHistory.self, from: data)
#expect(!history.found)
#expect(history.petId == "pet_external")
#expect(history.jobId == nil)
#expect(history.messages.isEmpty)
#expect(history.revisionId == nil)
#expect(history.validationSummary == nil)
}
@Test("Maker history detail exposes only a verified routable Studio thread")
func studioHistoryDetailDecodesVerifiedNavigation() throws {
let data = Data(#"""
{
"ok":true,
"found":true,
"job_id":"job_history",
"status":"failed",
"operation":"create",
"description":"A small fox",
"style":"半写实",
"quality":"standard",
"reference_count":1,
"created_at":"2026-08-07T00:00:00Z",
"updated_at":"2026-08-07T00:01:00Z",
"progress_messages":[{"id":"msg_progress","job_id":"job_history","sequence":2,"role":"assistant","kind":"generation_progress","content":"Preparing","progress":0.1,"created_at":"2026-08-07T00:00:01Z"}],
"latest_codex_excerpt":"Bounded excerpt",
"message_count":3,
"messages_truncated":false,
"session":{"availability":"available","can_open":true,"routable_session_id":"019f5b0f-88ff-7413-8953-29de4ed0951c","name":"Agent Pet Studio · A small fox"}
}
"""#.utf8)
let detail = try JSONDecoder().decode(GenerationStudioHistoryDetail.self, from: data)
#expect(detail.found)
#expect(detail.status == .failed)
#expect(detail.referenceCount == 1)
#expect(detail.progressMessages.map(\.id) == ["msg_progress"])
#expect(detail.session.availability == .available)
#expect(detail.session.canOpen)
#expect(
detail.session.routableSessionID
== "019f5b0f-88ff-7413-8953-29de4ed0951c"
)
}
@Test("input_request remains an active generation session")
func inputRequestRemainsActive() {
var session = runningSession()
let effects = session.reduce(.messagesReceived(
[message(id: "msg_input", kind: "input_request", progress: 0.24)],
revision: "2"
))
#expect(session.state == .waitingForInput)
#expect(session.isActive)
#expect(session.canCancel)
#expect(session.canSendReply)
#expect(!effects.contains(.stopMessageStream))
}
@Test("a waiting job restores after restart and resumes its stream")
func waitingJobRestoresAfterRestart() {
let inputRequest = message(id: "msg_restore", kind: "input_request", progress: 0.31)
let restore = GenerationSessionRestore(
state: .waitingForInput,
jobID: "job_restore",
submittedForm: form(description: "Original submitted prompt"),
messages: [inputRequest],
progress: 0.31,
messageRevision: "7"
)
var session = GenerationSession()
let effects = session.reduce(.restore(restore))
#expect(session.state == .waitingForInput)
#expect(session.jobID == "job_restore")
#expect(session.submittedForm?.description == "Original submitted prompt")
#expect(session.messages.map(\.id) == ["msg_restore"])
#expect(session.canCancel)
#expect(effects.contains(.startMessageStream))
}
@Test("latest Maker recovery restores every persisted terminal state without a pet ID")
func latestTerminalSessionRestoresAfterRestart() throws {
for (rawStatus, expectedState) in [
("failed", GenerationSessionState.failed),
("canceled", .cancelled),
("completed", .succeeded),
] {
let data = Data(#"""
{
"found":true,
"job_id":"job_\#(rawStatus)",
"status":"\#(rawStatus)",
"result_pet_id":null,
"operation":"create",
"form":{"description":"Recovered \#(rawStatus)","style":"像素","quality":"standard","reference_images":[]},
"message_revision":"9",
"messages":[{"id":"msg_\#(rawStatus)","role":"assistant","kind":"generation_\#(rawStatus)","content":"terminal","progress":1,"created_at":"2026-07-22T00:00:00Z"}]
}
"""#.utf8)
let snapshot = try JSONDecoder().decode(
LatestGenerationSessionSnapshot.self,
from: data
)
let restore = try #require(GenerationSessionRestore(snapshot: snapshot))
#expect(restore.state == expectedState)
#expect(restore.jobID == "job_\(rawStatus)")
#expect(restore.resultPetID == nil)
#expect(restore.submittedForm?.description == "Recovered \(rawStatus)")
#expect(restore.messageRevision == "9")
#expect(restore.progress == 1)
}
}
@Test("an empty latest Maker response cannot create a phantom session")
func emptyLatestSessionDoesNotRestore() throws {
let snapshot = try JSONDecoder().decode(
LatestGenerationSessionSnapshot.self,
from: Data(#"{"found":false,"messages":[]}"#.utf8)
)
#expect(GenerationSessionRestore(snapshot: snapshot) == nil)
}
@Test("recovery projections decode a bounded reference reselection count compatibly")
func recoveryReferenceReselectionCountIsBoundedAndCompatible() throws {
let latest = try JSONDecoder().decode(
LatestGenerationSessionSnapshot.self,
from: Data(#"{"found":true,"job_id":"job_refs","status":"failed","form":{"description":"Retry","style":"像素","quality":"standard","reference_images":[]},"reference_reselection_count":2,"messages":[]}"#.utf8)
)
let history = try JSONDecoder().decode(
GenerationHistory.self,
from: Data(#"{"found":true,"pet_id":"pet_refs","reference_reselection_count":2,"form":{"description":"Retry","style":"像素","quality":"standard","reference_images":[]},"messages":[]}"#.utf8)
)
let active = try JSONDecoder().decode(
ActiveGenerationSnapshot.self,
from: Data(#"{"job_id":"job_refs","status":"waiting_for_user","form":{"description":"Retry","style":"像素","quality":"standard","reference_images":[]},"heartbeat_at":"2026-07-22T00:00:00Z","message_revision":"1","reference_reselection_count":2,"messages":[]}"#.utf8)
)
let legacyLatest = try JSONDecoder().decode(
LatestGenerationSessionSnapshot.self,
from: Data(#"{"found":false,"messages":[]}"#.utf8)
)
#expect(latest.referenceReselectionCount == 2)
#expect(try #require(GenerationSessionRestore(snapshot: latest))
.referenceReselectionCount == 2)
#expect(history.referenceReselectionCount == 2)
#expect(active.referenceReselectionCount == 2)
#expect(GenerationSessionRestore(snapshot: active).referenceReselectionCount == 2)
#expect(legacyLatest.referenceReselectionCount == 0)
for malformed in [
#"{"found":false,"reference_reselection_count":-1,"messages":[]}"#,
#"{"found":false,"reference_reselection_count":5,"messages":[]}"#,
#"{"found":true,"job_id":"job_refs","status":"failed","form":{"description":"Retry","style":"像素","quality":"standard","reference_images":["/private/tmp/original.png"]},"reference_reselection_count":1,"messages":[]}"#,
] {
#expect(throws: DecodingError.self) {
_ = try JSONDecoder().decode(
LatestGenerationSessionSnapshot.self,
from: Data(malformed.utf8)
)
}
}
}
@Test("daemon active_generation decodes into a resumable waiting session")
func daemonActiveGenerationSnapshotDecodes() throws {
let data = Data(#"""
{
"job_id":"job_snapshot",
"status":"waiting_for_user",
"form":{"description":"Snapshot prompt","style":"半写实","quality":"low","reference_images":[]},
"session_id":"session_snapshot",
"result_pet_id":"pet_existing",
"operation":"modify",
"baseline_revision_id":"rev_11111111111111111111111111111111",
"owner_instance_id":"instance_old",
"heartbeat_at":"2026-07-10T00:00:00Z",
"message_revision":"11",
"messages":[{"id":"msg_snapshot","job_id":"job_snapshot","sequence":11,"role":"assistant","kind":"input_request","content":"Choose a color","progress":0.3,"created_at":"2026-07-10T00:00:00Z"}],
"input_request":{"id":"msg_snapshot","job_id":"job_snapshot","sequence":11,"role":"assistant","kind":"input_request","content":"Choose a color","progress":0.3,"created_at":"2026-07-10T00:00:00Z"}
}
"""#.utf8)
let snapshot = try JSONDecoder().decode(ActiveGenerationSnapshot.self, from: data)
let restore = GenerationSessionRestore(snapshot: snapshot)
#expect(snapshot.status == .waitingForUser)
#expect(snapshot.ownerInstanceID == "instance_old")
#expect(restore.state == .waitingForInput)
#expect(restore.jobID == "job_snapshot")
#expect(restore.submittedForm?.description == "Snapshot prompt")
#expect(restore.messages.map(\.id) == ["msg_snapshot"])
#expect(restore.messageRevision == "11")
#expect(restore.operation == .modify)
#expect(restore.resultPetID == "pet_existing")
#expect(restore.baselineRevisionID == "rev_11111111111111111111111111111111")
}
@Test("daemon active_generation decodes a recoverable failed session")
func daemonRecoverableFailureSnapshotDecodes() throws {
let data = Data(#"""
{
"job_id":"job_recoverable",
"status":"failed",
"form":{"description":"Preserved pet","style":"半写实","quality":"standard","reference_images":[]},
"heartbeat_at":"2026-08-09T03:54:42Z",
"recoverable":true,
"failure_code":"generation_failed",
"pause_reason":"checkpoint paused",
"message_revision":"156",
"messages":[]
}
"""#.utf8)
let snapshot = try JSONDecoder().decode(ActiveGenerationSnapshot.self, from: data)
let restore = GenerationSessionRestore(snapshot: snapshot)
#expect(snapshot.status == .failed)
#expect(restore.state == .recoverableFailed)
#expect(restore.recoverable)
#expect(restore.pauseReason == "checkpoint paused")
#expect(restore.messageRevision == "156")
}
@Test("an edit session preserves its target across start, failure, and retry setup")
func editSessionPreservesTargetIdentity() {
var session = GenerationSession()
let submitted = form(description: "Modify the existing pet")
_ = session.reduce(.editRequested(
form: submitted,
initialMessage: message(id: "msg_edit", progress: 0.01),
petID: "pet_existing",
baselineRevisionID: "rev_11111111111111111111111111111111"
))
_ = session.reduce(.startAccepted(jobID: "job_edit"))
_ = session.reduce(.messagesReceived(
[message(id: "msg_failed", kind: "generation_failed", progress: 1)],
revision: "2"
))
#expect(session.state == .failed)
#expect(session.operation == .modify)
#expect(session.resultPetID == "pet_existing")
#expect(session.baselineRevisionID == "rev_11111111111111111111111111111111")
#expect(session.canRetry)
_ = session.reduce(.retryRequested(
form: submitted,
initialMessage: message(id: "msg_retry", progress: 0.01)
))
#expect(session.state == .starting)
#expect(session.jobID == "job_edit")
#expect(session.operation == .modify)
#expect(session.resultPetID == "pet_existing")
#expect(session.baselineRevisionID == "rev_11111111111111111111111111111111")
_ = session.reduce(.startFailed(message: message(
id: "msg_retry_failed",
kind: "generation_failed",
progress: 1
)))
#expect(session.state == .failed)
#expect(session.jobID == "job_edit")
#expect(session.baselineRevisionID == "rev_11111111111111111111111111111111")
#expect(session.canRetry)
}
@Test("resuming a failed job preserves its job and complete message history")
func resumePreservesJobAndMessages() {
var session = GenerationSession()
let submitted = form(description: "Resume me")
_ = session.reduce(.startRequested(
form: submitted,
initialMessage: message(id: "msg_user", progress: 0.01)
))
_ = session.reduce(.startAccepted(jobID: "job_resume"))
let failedMessages = [
message(id: "msg_user", progress: 0.01),
message(id: "msg_checkpoint", kind: "generation_checkpoint", progress: 0.13),
message(id: "msg_failed", kind: "generation_failed", progress: 1),
]
_ = session.reduce(.messagesReceived(failedMessages, revision: "3"))
_ = session.reduce(.restore(GenerationSessionRestore(
state: .recoverableFailed,
jobID: "job_resume",
submittedForm: submitted,
messages: failedMessages,
progress: 0.13,
messageRevision: "3",
recoverable: true,
failureCode: "checkpoint_stalled",
pauseReason: "The last verified checkpoint is retained.",
capabilities: GenerationSessionCapabilities(
canResume: true,
canCancel: true
)
)))
#expect(session.canResume)
_ = session.reduce(.resumeRequested)
#expect(session.state == .starting)
#expect(session.jobID == "job_resume")
#expect(session.messages == failedMessages)
#expect(session.messageRevision == "3")
let effects = session.reduce(.startAccepted(jobID: "job_resume"))
#expect(session.state == .running)
#expect(effects.contains(.startMessageStream))
}
@Test("typed runtime activity drives stages even while numeric progress stays low")
func typedRuntimeActivityDrivesStages() {
let generating = [
message(id: "msg_generate", kind: "generation_activity_generating", progress: 0.11),
]
let validating = [
message(id: "msg_validate", kind: "generation_activity_validating", progress: 0.14),
]
let importing = [
message(id: "msg_import", kind: "generation_activity_importing", progress: 0.90),
]
#expect(GenerationConversation.activeStepIndex(
messages: generating,
progress: 0.11,
operation: .create
) == 1)
#expect(GenerationConversation.activeStepIndex(
messages: validating,
progress: 0.14,
operation: .create
) == 2)
#expect(GenerationConversation.activeStepIndex(
messages: importing,
progress: 0.90,
operation: .create
) == 3)
#expect(GenerationConversation.activeStepIndex(
messages: generating,
progress: 0.11,
operation: .modify
) == 2)
#expect(GenerationConversation.checkpointCount([
message(id: "checkpoint-1", kind: "generation_checkpoint", progress: 0.13),
message(id: "heartbeat", kind: "generation_heartbeat", progress: 0.13),
message(id: "checkpoint-2", kind: "generation_checkpoint", progress: 0.13),
]) == 2)
}
@Test("reconciling the same active snapshot does not restart its stream")
func repeatedSnapshotDoesNotRestartStream() {
let restore = GenerationSessionRestore(
state: .running,
jobID: "job_same",
submittedForm: form(description: "Same job"),
messages: [message(id: "msg_same", progress: 0.4)],
progress: 0.4,
messageRevision: "4"
)
var session = GenerationSession()
let first = session.reduce(.restore(restore))
let second = session.reduce(.restore(restore))
#expect(first.contains(.startMessageStream))
#expect(!second.contains(.startMessageStream))
#expect(!second.contains(.stopMessageStream))
}
@Test("a submitted form cannot be replaced while the session is active")
func activeFormIsImmutable() {
var session = GenerationSession()
let first = form(description: "Original submitted prompt")
let replacement = form(description: "Edited draft must not leak")
_ = session.reduce(.startRequested(
form: first,
initialMessage: message(id: "msg_first", progress: 0.05)
))
_ = session.reduce(.startRequested(
form: replacement,
initialMessage: message(id: "msg_second", progress: 0.05)
))
#expect(session.state == .starting)
#expect(session.submittedForm == first)
#expect(session.messages.map(\.id) == ["msg_first"])
}
@Test("waiting sessions can be cancelled")
func waitingCanCancel() {
var session = runningSession()
_ = session.reduce(.messagesReceived(
[message(id: "msg_wait", kind: "input_request", progress: 0.20)],
revision: "2"
))
#expect(session.canCancel)
_ = session.reduce(.cancelRequested)
#expect(session.state == .cancelling)
#expect(session.isActive)
}
@Test("generation messages retain daemon IDs across repeated decoding")
func messageIdentityIsStable() throws {
let withID = Data(#"{"id":"msg_stable","role":"assistant","content":"same","progress":0.5,"created_at":"2026-07-10T00:00:00Z"}"#.utf8)
let legacy = Data(#"{"role":"assistant","content":"legacy","progress":0.5,"created_at":"2026-07-10T00:00:00Z"}"#.utf8)
let first = try JSONDecoder().decode(GenerationMessage.self, from: withID)
let second = try JSONDecoder().decode(GenerationMessage.self, from: withID)
let legacyFirst = try JSONDecoder().decode(GenerationMessage.self, from: legacy)
let legacySecond = try JSONDecoder().decode(GenerationMessage.self, from: legacy)
#expect(first.id == "msg_stable")
#expect(second.id == first.id)
#expect(legacyFirst.id == legacySecond.id)
#expect(legacyFirst.id.hasPrefix("msg_legacy_"))
}
@Test("localized message copy never infers a terminal generation state")
func terminalInferenceRequiresTypedKinds() {
let localizedOnly = [
GenerationMessage(
role: "assistant",
content: "完成,可在宠物库启用。",
progress: 1,
createdAt: "2026-07-10T00:00:00Z"
),
]
let renamedCopyOnly = [
GenerationMessage(
role: "assistant",
content: "Finished and ready to use.",
progress: 1,
createdAt: "2026-07-10T00:00:00Z"
),
]
for messages in [localizedOnly, renamedCopyOnly] {
#expect(!GenerationConversation.succeeded(messages))
#expect(!GenerationConversation.cancelled(messages))
#expect(!GenerationConversation.failed(messages))
#expect(!GenerationConversation.terminalUnsuccessful(messages))
}
}
@Test("cancellation preserves the last real generation stage")
func cancellationPreservesTheLastRealStage() {
let canceledDuringBrief = [
message(id: "msg_brief", kind: "generation_progress", progress: 0.10),
message(id: "msg_cancelled", kind: "generation_canceled", progress: 1),
]
let canceledDuringGeneration = [
message(id: "msg_generation", kind: "generation_progress", progress: 0.40),
message(id: "msg_cancelled_later", kind: "generation_canceled", progress: 1),
]
#expect(GenerationConversation.activeStepIndex(
messages: canceledDuringBrief,
progress: 1
) == 0)
#expect(GenerationConversation.activeStepIndex(
messages: canceledDuringGeneration,
progress: 1
) == 1)
}
@Test("generation history decodes terminal status into a typed bounded value")
func generationHistoryStatusIsTyped() throws {
for (rawStatus, expected) in [
("completed", GenerationJobHistoryStatus.completed),
("cancelled", GenerationJobHistoryStatus.canceled),
("failed", GenerationJobHistoryStatus.failed),
] {
let data = Data(
"{\"found\":true,\"pet_id\":\"pet_fixture\",\"status\":\"\(rawStatus)\"}"
.utf8
)
let history = try JSONDecoder().decode(GenerationHistory.self, from: data)
#expect(history.status == expected)
}
let unknown = try JSONDecoder().decode(
GenerationHistory.self,
from: Data(
#"{"found":true,"pet_id":"pet_fixture","status":"future_status"}"#.utf8
)
)
#expect(unknown.status == nil)
}
@Test("a terminal job requests stream shutdown exactly once")
func terminalJobStopsStreamOnce() {
var session = runningSession()
let completed = [message(id: "msg_done", kind: "generation_completed", progress: 1)]
let firstEffects = session.reduce(.messagesReceived(completed, revision: "3"))
let secondEffects = session.reduce(.messagesReceived(completed, revision: "3"))
#expect(session.state == .succeeded)
#expect(firstEffects.contains(.stopMessageStream))
#expect(firstEffects.contains(.refreshSnapshot))
#expect(!secondEffects.contains(.stopMessageStream))
#expect(!secondEffects.contains(.refreshSnapshot))
#expect(!session.canSendReply)
#expect(!GenerationConversation.canSendReply(completed))
}
@Test("typed terminal metadata restores the real pet and revision independently")
func terminalMetadataRestoresWithoutConfusingPetAndRevisionIDs() throws {
let data = Data(#"""
{
"found":true,
"pet_id":"pet_created",
"job_id":"job_created",
"status":"completed",
"result_pet_id":"pet_created",
"revision_id":"rev_0123456789abcdef0123456789abcdef",
"validation_summary":{"ok":true,"state_count":9,"frame_count":120,"warning_count":0},
"messages":[]
}
"""#.utf8)
let history = try JSONDecoder().decode(GenerationHistory.self, from: data)
let restore = GenerationSessionRestore(
state: .succeeded,
jobID: try #require(history.jobId),
submittedForm: history.form,
messages: history.messages,
progress: 1,
messageRevision: "",
resultPetID: history.resultPetId,
resultRevisionID: history.revisionId,
validationSummary: history.validationSummary
)
var session = GenerationSession()
_ = session.reduce(.restore(restore))
#expect(session.resultPetID == "pet_created")
#expect(session.resultRevisionID == "rev_0123456789abcdef0123456789abcdef")
#expect(session.resultPetID != session.resultRevisionID)
#expect(session.validationSummary?.stateCount == 9)
#expect(session.validationSummary?.frameCount == 120)
#expect(!session.canSendReply)
}
@Test("a create session accepts typed result metadata when the terminal wait response arrives")
func createSessionAcceptsTerminalResultMetadata() {
var session = runningSession()
_ = session.reduce(.messagesReceived(
[message(id: "msg_done", kind: "generation_completed", progress: 1)],
revision: "3"
))
_ = session.reduce(.resultMetadataReceived(GenerationResultMetadata(
resultPetID: "pet_result",
revisionID: "rev_0123456789abcdef0123456789abcdef",
validationSummary: GenerationValidationSummary(
ok: true,
stateCount: 7,
frameCount: 120,
warningCount: 0
)
)))
#expect(session.state == .succeeded)
#expect(session.resultPetID == "pet_result")
#expect(session.resultRevisionID == "rev_0123456789abcdef0123456789abcdef")
#expect(session.validationSummary?.ok == true)
}
@Test("a structured reply explicitly reactivates a waiting session")
func replyReactivatesWaitingSession() {
var session = runningSession()
_ = session.reduce(.messagesReceived(
[message(id: "msg_question", kind: "input_request", progress: 0.25)],
revision: "2"
))
_ = session.reduce(.replySubmitted)
#expect(session.state == .running)
#expect(session.isActive)
#expect(!session.canSendReply)
#expect(session.canCancel)
}
@Test("generation states are exhaustive and only lifecycle work is active")
func stateCasesAndActivityAreStrict() {
#expect(GenerationSessionState.allCases == [
.idle,
.starting,
.running,
.waitingForInput,
.paused,
.recoverableFailed,
.cancelling,
.cancelCleanup,
.succeeded,
.failed,
.cancelled,
])
#expect(!GenerationSessionState.idle.isActive)
#expect(GenerationSessionState.starting.isActive)
#expect(GenerationSessionState.running.isActive)
#expect(GenerationSessionState.waitingForInput.isActive)
#expect(GenerationSessionState.paused.isActive)
#expect(GenerationSessionState.recoverableFailed.isActive)
#expect(GenerationSessionState.cancelling.isActive)
#expect(GenerationSessionState.cancelCleanup.isActive)
#expect(!GenerationSessionState.succeeded.isActive)
#expect(!GenerationSessionState.failed.isActive)
#expect(!GenerationSessionState.cancelled.isActive)
}
private func runningSession() -> GenerationSession {
var session = GenerationSession()
_ = session.reduce(.startRequested(
form: form(description: "Submitted"),
initialMessage: message(id: "msg_user", progress: 0.05)
))
_ = session.reduce(.startAccepted(jobID: "job_active"))
return session
}
private func form(description: String) -> GenerationForm {
GenerationForm(
description: description,
style: "半写实",
quality: .low,
referenceImages: ["/tmp/reference.png"]
)
}
private func message(
id: String,
kind: String? = nil,
progress: Double
) -> GenerationMessage {
GenerationMessage(
id: id,
role: "assistant",
content: kind ?? "working",
progress: progress,
createdAt: "2026-07-10T00:00:00Z",
kind: kind
)
}
}