packages feed

kioku-core 0.1.0.0 → 0.2.0.0

raw patch · 4 files changed

+377/−18 lines, 4 filesdep ~baikaidep ~baikai-claudedep ~baikai-effectfulPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: baikai, baikai-claude, baikai-effectful, keiki, keiro, keiro-core, kioku-api, kioku-core, kioku-migrations, shikumi, shikumi-trace

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,40 @@ # Changelog +## 0.2.0.0 — 2026-07-30++### Breaking Changes++- Moved onto the Keiki 0.4 and Keiro 0.4 cohort: `keiki ^>=0.4.0.0`, `keiro ^>=0.4.0.1`, and+  `keiro-core ^>=0.4.0.1`. Kioku no longer builds against Keiki 0.2 or Keiro 0.3, and there is+  no version of `kioku-core` that spans both.+- Keiro 0.4 tightens validated event-stream assembly: `mkEventStreamOrThrow` now rejects a codec+  whose schema version, event tags, or upcaster chain fail `mkCodec`, and it does so at+  evaluation time. Kioku's `memoryEventStream` and `sessionEventStream` both satisfy the stricter+  contract, but a consumer that builds its own streams alongside Kioku's will see the new+  validation at startup.+- A consumer that links `kioku-core` now links Keiro 0.4's API changes directly, including the+  snapshot `state_shape_hash` invalidation and the timer and workflow-children changes. Kioku+  itself calls none of them — both of its event streams set `snapshotPolicy = Never` and+  `stateCodec = Nothing`, and it has no durable workflows — but the types are on the consumer's+  path.+- Moved onto the Baikai 0.4 / Shikumi 0.3.0.1 cohort: `baikai ^>=0.4.1.0`,+  `baikai-claude ^>=0.4.0.1`, `baikai-effectful ^>=0.3.0.2`, `shikumi ^>=0.3.0.1`, and+  `shikumi-trace ^>=0.2.0.1`. The Shikumi bound names an exact patch level deliberately:+  `shikumi-0.3.0.0` is pinned to the Baikai 0.3 series and would let the solver fall off the+  intended cohort silently.++### Changed++- The connection-settings path is unchanged. `Kioku.App.AppEnv.connectionSettings` still carries+  a `Kiroku.Store.Connection.ConnectionSettings` and `runAppIO` still acquires the store resource+  from it; `kiroku-store` does not move major versions in this cohort.++### Added++- Added `Kioku.CodecCompatSpec` to the test-suite: thirteen literal pre-upgrade JSON payloads —+  one per `MemoryEvent` and `SessionEvent` constructor — asserted to decode under the new cohort,+  plus two tests pinning the native and legacy arms of `parseMemoryEvent`'s fallback.+ ## 0.1.0.0 — 2026-07-14  ### Added
kioku-core.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            kioku-core-version:         0.1.0.0+version:         0.2.0.0 synopsis:        Reusable agent memory runtime description:   Core runtime for kioku. M1 establishes the application effect stack; later@@ -73,11 +73,17 @@     Kioku.Session.ReadModel     Kioku.Worker.Failure +  -- Two bounds below look wrong but are not. baikai-effectful has no 0.4+  -- release; 0.3.0.2 is its newest and is itself built against baikai ^>=0.4.0,+  -- so its version lags the rest of the Baikai cohort by design. And shikumi is+  -- pinned at ^>=0.3.0.1 rather than ^>=0.3.0.0 because only that patch carries+  -- the baikai 0.4 bounds -- 0.3.0.0 is tied to baikai 0.3 and would give the+  -- solver a way to silently fall off this cohort.   build-depends:     , aeson                   >=2.2       && <2.3-    , baikai                  ^>=0.3.0.0-    , baikai-claude           ^>=0.3.0.0-    , baikai-effectful        ^>=0.3.0.0+    , baikai                  ^>=0.4.1.0+    , baikai-claude           ^>=0.4.0.1+    , baikai-effectful        ^>=0.3.0.2     , base                    >=4.21      && <5     , bytestring              >=0.11      && <0.13     , containers              >=0.6       && <0.8@@ -93,17 +99,17 @@     , hasql-pool              >=1.2       && <1.5     , hasql-transaction       >=1.0       && <1.3     , hs-opentelemetry-api    >=1.0       && <1.1-    , keiki                   ^>=0.2.0.0-    , keiro                   ^>=0.3.0.0-    , keiro-core              ^>=0.3.0.0-    , kioku-api               ^>=0.1.0.0+    , keiki                   ^>=0.4.0.0+    , keiro                   ^>=0.4.0.1+    , keiro-core              ^>=0.4.0.1+    , kioku-api               ^>=0.2.0.0     , kiroku-store            ^>=0.3.0.1     , lens                    >=5.2       && <5.4     , mmzk-typeid             >=0.7       && <0.8     , shibuya-core            >=0.8.0.1   && <0.9     , shibuya-kiroku-adapter  ^>=0.4.0.0-    , shikumi                 ^>=0.3.0.0-    , shikumi-trace           ^>=0.2.0.0+    , shikumi                 ^>=0.3.0.1+    , shikumi-trace           ^>=0.2.0.1     , text                    >=2.1       && <2.2     , time                    >=1.12      && <1.15     , uuid                    >=1.3       && <1.4@@ -115,6 +121,7 @@   main-is:        Main.hs   other-modules:     Kioku.AwaitingSpec+    Kioku.CodecCompatSpec     Kioku.DistillSpec     Kioku.EmbeddingWorkerSpec     Kioku.IdempotencySpec@@ -133,7 +140,7 @@   ghc-options:    -threaded -rtsopts -with-rtsopts=-N   build-depends:     , aeson                          >=2.2-    , baikai                         ^>=0.3.0.0+    , baikai                         ^>=0.4.1.0     , base                           >=4.21     && <5     , bytestring                     >=0.11     , containers                     >=0.6@@ -143,16 +150,16 @@     , effectful-core                 >=2.5     , hasql                          >=1.6     , hasql-transaction              >=1.0-    , keiro                          ^>=0.3.0.0-    , keiro-core                     ^>=0.3.0.0-    , kioku-api                      ^>=0.1.0.0-    , kioku-core                     ^>=0.1.0.0-    , kioku-migrations:test-support  ^>=0.1.0.0+    , keiro                          ^>=0.4.0.1+    , keiro-core                     ^>=0.4.0.1+    , kioku-api                      ^>=0.2.0.0+    , kioku-core                     ^>=0.2.0.0+    , kioku-migrations:test-support  ^>=0.2.0.0     , kiroku-store                   ^>=0.3.0.1     , lens                           >=5.2     , shibuya-core                   >=0.8.0.1  && <0.9-    , shikumi                        ^>=0.3.0.0-    , shikumi-trace                  ^>=0.2.0.0+    , shikumi                        ^>=0.3.0.1+    , shikumi-trace                  ^>=0.2.0.1     , tasty                          >=1.5     , tasty-expected-failure         >=0.12     , tasty-hunit                    >=0.10
+ test/Kioku/CodecCompatSpec.hs view
@@ -0,0 +1,315 @@+-- | Pins the on-the-wire shape of every event Kioku persists.+--+-- Keiro 0.4 tightened validated event-stream assembly, so the risk worth holding+-- still is that a payload written by an earlier Kioku stops decoding. Each+-- fixture below is the literal output of the live codec's @encode@ for one+-- constructor, captured from the pre-upgrade tree; the assertions decode them+-- through the same @parseMemoryEvent@ / @parseSessionEvent@ the event store uses+-- and check the round-tripped value field by field.+--+-- 'Kioku.ReiCompatSpec' covers the /legacy/ arm of those parsers -- payloads in+-- the older Rei wire format. This module covers the /native/ arm, and the last+-- test here pins the fallback that joins them, so a future upgrade cannot delete+-- the legacy path without a test going red.+module Kioku.CodecCompatSpec+  ( tests,+  )+where++import Control.Monad ((<=<))+import Data.Aeson (Value, eitherDecode)+import Data.Bifunctor (first)+import Data.ByteString.Lazy (ByteString)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime)+import Data.Time.Format.ISO8601 (iso8601ParseM)+import Kioku.Api.Scope (MemoryScope (..), Namespace (..), ScopeKind (..))+import Kioku.Api.Types (Confidence (..), MemoryType (..))+import Kioku.Id (idText)+import Kioku.Memory.Domain+  ( MemoryArchivedData (..),+    MemoryConfidenceUpdatedData (..),+    MemoryEvent (..),+    MemoryMergedData (..),+    MemoryRecordedData (..),+    MemorySupersededData (..),+    MemoryTagsUpdatedData (..),+  )+import Kioku.Memory.EventStream (parseMemoryEvent)+import Kioku.Session.Domain+  ( InteractiveSessionRecordedData (..),+    SessionAwaitingData (..),+    SessionCompletedData (..),+    SessionEvent (..),+    SessionFailedData (..),+    SessionResumedData (..),+    SessionStartedData (..),+    TurnRecordedData (..),+  )+import Kioku.Session.EventStream (parseSessionEvent)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))++tests :: TestTree+tests =+  testGroup+    "pre-upgrade event payloads still decode"+    [ testGroup "memory events" memoryTests,+      testGroup "session events" sessionTests,+      testGroup "the legacy fallback still exists" fallbackTests+    ]++-- * Memory events++memoryTests :: [TestTree]+memoryTests =+  [ testCase "memory_recorded" do+      decodeMemory memoryRecordedJson >>= \case+        MemoryRecorded d -> do+          idText d.memoryId @?= memoryIdText+          d.agentId @?= "agent-1"+          fmap idText d.sessionId @?= Just sessionIdText+          d.scope @?= fixtureScope+          d.memoryType @?= MemoryFact+          d.content @?= "the build is green"+          d.priority @?= 3+          d.confidence @?= HighConfidence+          d.tags @?= Set.fromList ["build", "ci"]+          d.supersedes @?= Nothing+          d.recordedAt @?= at "2026-06-24T21:30:00Z"+        other -> unexpected "MemoryRecorded" other,+    testCase "memory_superseded" do+      decodeMemory memorySupersededJson >>= \case+        MemorySuperseded d -> do+          idText d.memoryId @?= memoryIdText+          idText d.supersededBy @?= otherMemoryIdText+          d.supersededAt @?= at "2026-06-24T21:31:00Z"+        other -> unexpected "MemorySuperseded" other,+    testCase "memory_archived" do+      decodeMemory memoryArchivedJson >>= \case+        MemoryArchived d -> do+          idText d.memoryId @?= memoryIdText+          d.archivedAt @?= at "2026-06-24T21:32:00Z"+        other -> unexpected "MemoryArchived" other,+    testCase "memory_tags_updated" do+      decodeMemory memoryTagsUpdatedJson >>= \case+        MemoryTagsUpdated d -> do+          idText d.memoryId @?= memoryIdText+          d.tags @?= Set.fromList ["ci", "stable"]+          d.updatedAt @?= at "2026-06-24T21:33:00Z"+        other -> unexpected "MemoryTagsUpdated" other,+    testCase "memory_confidence_updated" do+      decodeMemory memoryConfidenceUpdatedJson >>= \case+        MemoryConfidenceUpdated d -> do+          idText d.memoryId @?= memoryIdText+          d.confidence @?= MediumConfidence+          d.updatedAt @?= at "2026-06-24T21:34:00Z"+        other -> unexpected "MemoryConfidenceUpdated" other,+    testCase "memory_merged" do+      decodeMemory memoryMergedJson >>= \case+        MemoryMerged d -> do+          idText d.memoryId @?= memoryIdText+          idText d.mergedInto @?= otherMemoryIdText+          d.mergedAt @?= at "2026-06-24T21:35:00Z"+        other -> unexpected "MemoryMerged" other+  ]++-- * Session events++sessionTests :: [TestTree]+sessionTests =+  [ testCase "session_started" do+      decodeSession sessionStartedJson >>= \case+        SessionStarted d -> do+          idText d.sessionId @?= sessionIdText+          d.agentId @?= "agent-1"+          d.focus @?= "ship the release"+          d.scope @?= fixtureScope+          d.subjectRef @?= Just "kioku"+          d.previousSessionId @?= Nothing+          d.parentSessionId @?= Nothing+          d.delegationDepth @?= 0+          d.startedAt @?= at "2026-06-24T21:30:00Z"+        other -> unexpected "SessionStarted" other,+    testCase "session_completed" do+      decodeSession sessionCompletedJson >>= \case+        SessionCompleted d -> do+          idText d.sessionId @?= sessionIdText+          d.completedAt @?= at "2026-06-24T21:40:00Z"+          d.modelUsed @?= Just "claude-opus-4-8"+          d.summary @?= Just "planned the day"+        other -> unexpected "SessionCompleted" other,+    testCase "session_failed" do+      decodeSession sessionFailedJson >>= \case+        SessionFailed d -> do+          idText d.sessionId @?= sessionIdText+          d.failedAt @?= at "2026-06-24T21:41:00Z"+          d.errorMessage @?= "model timed out"+        other -> unexpected "SessionFailed" other,+    testCase "session_awaiting" do+      decodeSession sessionAwaitingJson >>= \case+        SessionAwaiting d -> do+          idText d.sessionId @?= sessionIdText+          d.reason @?= "needs human input"+          d.correlationKey @?= Just "approval-1"+          d.deadline @?= Just (at "2026-06-25T21:41:00Z")+          d.awaitedAt @?= at "2026-06-24T21:42:00Z"+        other -> unexpected "SessionAwaiting" other,+    testCase "session_resumed" do+      decodeSession sessionResumedJson >>= \case+        SessionResumed d -> do+          idText d.sessionId @?= sessionIdText+          d.correlationKey @?= Just "approval-1"+          d.force @?= False+          d.input @?= "approved"+          d.resumedAt @?= at "2026-06-24T21:43:00Z"+        other -> unexpected "SessionResumed" other,+    testCase "interactive_session_recorded" do+      decodeSession interactiveSessionRecordedJson >>= \case+        InteractiveSessionRecorded d -> do+          idText d.sessionId @?= sessionIdText+          d.agentId @?= "agent-1"+          d.focus @?= "pairing"+          d.scope @?= fixtureScope+          d.subjectRef @?= Nothing+          d.startedAt @?= at "2026-06-24T21:44:00Z"+        other -> unexpected "InteractiveSessionRecorded" other,+    testCase "turn_recorded" do+      decodeSession turnRecordedJson >>= \case+        TurnRecorded d -> do+          idText d.sessionId @?= sessionIdText+          d.turnId @?= "turn-1"+          d.turnIndex @?= 0+          d.role @?= "user"+          d.content @?= "hello"+          d.toolSummary @?= Just "no tools"+          d.promptTokens @?= Just 12+          d.outputTokens @?= Just 34+          d.recordedAt @?= at "2026-06-24T21:45:00Z"+        other -> unexpected "TurnRecorded" other+  ]++-- * The two-arm fallback++--+-- @parseMemoryEvent@ tries the native parser, then the legacy Rei one, and on a+-- double failure reports both errors. These pin all three behaviours so the+-- fallback cannot be dropped silently.++fallbackTests :: [TestTree]+fallbackTests =+  [ testCase "a legacy Rei payload decodes through the second arm" do+      decodeMemory legacyReiMemoryRecordedJson >>= \case+        MemoryRecorded d -> do+          idText d.memoryId @?= memoryIdText+          d.content @?= "recorded by rei"+        other -> unexpected "MemoryRecorded" other,+    testCase "an undecodable payload reports both arms' errors" do+      case parseMemoryEvent =<< decodeValue "{\"type\":\"not_a_real_event\",\"data\":{}}" of+        Right event -> assertFailure ("expected a decode failure, got " <> show event)+        Left err -> do+          assertContains "legacy decode failed" err+          assertContains "not_a_real_event" err+  ]++-- * Fixtures++--+-- Captured verbatim from @memoryCodec.encode@ / @sessionCodec.encode@ on the+-- pre-upgrade tree (kioku 0.1.0.0, Keiro 0.3). Do not regenerate these from the+-- current code -- that would make the test tautological. Edit only alongside a+-- deliberate, documented wire-format change.++memoryRecordedJson :: ByteString+memoryRecordedJson =+  "{\"data\":{\"agentId\":\"agent-1\",\"confidence\":\"high\",\"content\":\"the build is green\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"memoryType\":\"fact\",\"priority\":3,\"recordedAt\":\"2026-06-24T21:30:00Z\",\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"supersedes\":null,\"tags\":[\"build\",\"ci\"]},\"type\":\"memory_recorded\"}"++memorySupersededJson :: ByteString+memorySupersededJson =+  "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"supersededAt\":\"2026-06-24T21:31:00Z\",\"supersededBy\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgmf\"},\"type\":\"memory_superseded\"}"++memoryArchivedJson :: ByteString+memoryArchivedJson =+  "{\"data\":{\"archivedAt\":\"2026-06-24T21:32:00Z\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"memory_archived\"}"++memoryTagsUpdatedJson :: ByteString+memoryTagsUpdatedJson =+  "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"tags\":[\"ci\",\"stable\"],\"updatedAt\":\"2026-06-24T21:33:00Z\"},\"type\":\"memory_tags_updated\"}"++memoryConfidenceUpdatedJson :: ByteString+memoryConfidenceUpdatedJson =+  "{\"data\":{\"confidence\":\"medium\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"updatedAt\":\"2026-06-24T21:34:00Z\"},\"type\":\"memory_confidence_updated\"}"++memoryMergedJson :: ByteString+memoryMergedJson =+  "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"mergedAt\":\"2026-06-24T21:35:00Z\",\"mergedInto\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgmf\"},\"type\":\"memory_merged\"}"++sessionStartedJson :: ByteString+sessionStartedJson =+  "{\"data\":{\"agentId\":\"agent-1\",\"delegationDepth\":0,\"focus\":\"ship the release\",\"parentSessionId\":null,\"previousSessionId\":null,\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"startedAt\":\"2026-06-24T21:30:00Z\",\"subjectRef\":\"kioku\"},\"type\":\"session_started\"}"++sessionCompletedJson :: ByteString+sessionCompletedJson =+  "{\"data\":{\"completedAt\":\"2026-06-24T21:40:00Z\",\"modelUsed\":\"claude-opus-4-8\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"summary\":\"planned the day\"},\"type\":\"session_completed\"}"++sessionFailedJson :: ByteString+sessionFailedJson =+  "{\"data\":{\"errorMessage\":\"model timed out\",\"failedAt\":\"2026-06-24T21:41:00Z\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_failed\"}"++sessionAwaitingJson :: ByteString+sessionAwaitingJson =+  "{\"data\":{\"awaitedAt\":\"2026-06-24T21:42:00Z\",\"correlationKey\":\"approval-1\",\"deadline\":\"2026-06-25T21:41:00Z\",\"reason\":\"needs human input\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_awaiting\"}"++sessionResumedJson :: ByteString+sessionResumedJson =+  "{\"data\":{\"correlationKey\":\"approval-1\",\"force\":false,\"input\":\"approved\",\"resumedAt\":\"2026-06-24T21:43:00Z\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_resumed\"}"++interactiveSessionRecordedJson :: ByteString+interactiveSessionRecordedJson =+  "{\"data\":{\"agentId\":\"agent-1\",\"focus\":\"pairing\",\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"startedAt\":\"2026-06-24T21:44:00Z\",\"subjectRef\":null},\"type\":\"interactive_session_recorded\"}"++turnRecordedJson :: ByteString+turnRecordedJson =+  "{\"data\":{\"content\":\"hello\",\"outputTokens\":34,\"promptTokens\":12,\"recordedAt\":\"2026-06-24T21:45:00Z\",\"role\":\"user\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"toolSummary\":\"no tools\",\"turnId\":\"turn-1\",\"turnIndex\":0},\"type\":\"turn_recorded\"}"++-- | A Rei-format payload, which only the legacy arm of 'parseMemoryEvent' understands.+legacyReiMemoryRecordedJson :: ByteString+legacyReiMemoryRecordedJson =+  "{\"type\":\"agent_memory_recorded\",\"data\":{\"memoryId\":\"agent_memory_01kvxa7d2cezhs874g3n8dfgme\",\"agentId\":\"agent-1\",\"anchor\":{\"type\":\"intention\",\"id\":\"intention_demo\"},\"memoryType\":\"fact\",\"content\":\"recorded by rei\",\"confidence\":\"high\",\"tags\":[\"build\"],\"recordedAt\":\"2026-06-24T21:30:00Z\"}}"++-- * Helpers++fixtureScope :: MemoryScope+fixtureScope = ScopeEntity (Namespace "shikigami") (ScopeKind "repo") "kioku"++memoryIdText :: Text+memoryIdText = "kioku_memory_01kvxa7d2cezhs874g3n8dfgme"++otherMemoryIdText :: Text+otherMemoryIdText = "kioku_memory_01kvxa7d2cezhs874g3n8dfgmf"++sessionIdText :: Text+sessionIdText = "kioku_session_01kvxa7d2cezhs874g3n8dfgme"++at :: String -> UTCTime+at = maybe (error "CodecCompatSpec: malformed fixture timestamp") id . iso8601ParseM++decodeValue :: ByteString -> Either Text Value+decodeValue = first Text.pack . eitherDecode++decodeMemory :: ByteString -> IO MemoryEvent+decodeMemory = either (assertFailure . Text.unpack) pure . (parseMemoryEvent <=< decodeValue)++decodeSession :: ByteString -> IO SessionEvent+decodeSession = either (assertFailure . Text.unpack) pure . (parseSessionEvent <=< decodeValue)++unexpected :: (Show event) => String -> event -> IO ()+unexpected expected got = assertFailure ("Expected " <> expected <> ", got " <> show got)++assertContains :: Text -> Text -> IO ()+assertContains needle haystack =+  if needle `Text.isInfixOf` haystack+    then pure ()+    else assertFailure ("expected " <> show needle <> " in " <> show haystack)
test/Main.hs view
@@ -1,6 +1,7 @@ module Main where  import Kioku.AwaitingSpec qualified as AwaitingSpec+import Kioku.CodecCompatSpec qualified as CodecCompatSpec import Kioku.DistillSpec qualified as DistillSpec import Kioku.EmbeddingWorkerSpec qualified as EmbeddingWorkerSpec import Kioku.IdempotencySpec qualified as IdempotencySpec@@ -21,6 +22,7 @@     testGroup       "kioku"       [ AwaitingSpec.tests,+        CodecCompatSpec.tests,         ReiCompatSpec.tests,         IdempotencySpec.tests,         ReadModelReconcileSpec.tests,