b3-prosaic-harness-postgres 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/PKG-INFO +21 -2
  2. b3_prosaic_harness_postgres-0.3.0/src/b3_prosaic_harness_postgres.egg-info/PKG-INFO → b3_prosaic_harness_postgres-0.4.0/README.md +19 -20
  3. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/pyproject.toml +2 -2
  4. b3_prosaic_harness_postgres-0.3.0/README.md → b3_prosaic_harness_postgres-0.4.0/src/b3_prosaic_harness_postgres.egg-info/PKG-INFO +39 -0
  5. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/b3_prosaic_harness_postgres.egg-info/SOURCES.txt +1 -0
  6. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/b3_prosaic_harness_postgres.egg-info/requires.txt +1 -1
  7. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/__init__.py +1 -1
  8. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/cli.py +7 -1
  9. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/session_schema.py +31 -4
  10. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/session_store.py +7 -0
  11. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/local_cluster.py +6 -5
  12. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/session_documents.py +26 -0
  13. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_packaging.py +7 -7
  14. b3_prosaic_harness_postgres-0.4.0/tests/test_session_context.py +112 -0
  15. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/LICENSE +0 -0
  16. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/MANIFEST.in +0 -0
  17. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/NOTICE +0 -0
  18. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/setup.cfg +0 -0
  19. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/b3_prosaic_harness_postgres.egg-info/dependency_links.txt +0 -0
  20. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/b3_prosaic_harness_postgres.egg-info/entry_points.txt +0 -0
  21. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/b3_prosaic_harness_postgres.egg-info/top_level.txt +0 -0
  22. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/leases.py +0 -0
  23. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/schema.py +0 -0
  24. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/session_leases.py +0 -0
  25. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/store.py +0 -0
  26. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/src/prosaic_harness_postgres/transport.py +0 -0
  27. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/conftest.py +0 -0
  28. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/linux_wheel_smoke.py +0 -0
  29. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/model_server.py +0 -0
  30. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/session_workers.py +0 -0
  31. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/tcp_proxy.py +0 -0
  32. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/support/workers.py +0 -0
  33. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_cli.py +0 -0
  34. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_durability.py +0 -0
  35. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_engine.py +0 -0
  36. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_examples.py +0 -0
  37. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_leases.py +0 -0
  38. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_local_cluster.py +0 -0
  39. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_native_factory.py +0 -0
  40. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_recovery.py +0 -0
  41. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_registration.py +0 -0
  42. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_routing.py +0 -0
  43. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_schema.py +0 -0
  44. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_session_fencing.py +0 -0
  45. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_session_recovery.py +0 -0
  46. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_session_schema.py +0 -0
  47. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_session_store.py +0 -0
  48. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_session_workers.py +0 -0
  49. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_store.py +0 -0
  50. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_transport.py +0 -0
  51. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.0}/tests/test_workers.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-harness-postgres
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Explicit PostgreSQL run and conversation storage for Prosaic Harness
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: b3-prosaic-harness<0.9,>=0.8
10
+ Requires-Dist: b3-prosaic-harness<0.10,>=0.9
11
11
  Requires-Dist: psycopg[binary]<4,>=3.2.3
12
12
  Requires-Dist: psycopg-pool<4,>=3.2
13
13
  Provides-Extra: test
@@ -316,3 +316,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
316
316
  with `--require-postgres`. Missing infrastructure or any skipped required case
317
317
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
318
318
  before claiming the conversation candidate is ready for production.
319
+
320
+ ## Optional context storage contract
321
+
322
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
323
+ and v2 records, and preserves existing documents. New database initialization
324
+ creates contract 2. Existing contract-1 databases require an explicit migration;
325
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
326
+ then use migration-owner credentials:
327
+
328
+ ```sh
329
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
330
+ ```
331
+
332
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
333
+ The migration is locked, transactional and repeatable; restricted runtime roles
334
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
335
+ Keep migration-owner credentials separate from runtime credentials. Enable host
336
+ context policies only after Runtime, Harness and store advertise the capability.
337
+ No production token counter is bundled or certified by this storage adapter.
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: b3-prosaic-harness-postgres
3
- Version: 0.3.0
4
- Summary: Explicit PostgreSQL run and conversation storage for Prosaic Harness
5
- License-Expression: Apache-2.0
6
- Requires-Python: >=3.11
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- License-File: NOTICE
10
- Requires-Dist: b3-prosaic-harness<0.9,>=0.8
11
- Requires-Dist: psycopg[binary]<4,>=3.2.3
12
- Requires-Dist: psycopg-pool<4,>=3.2
13
- Provides-Extra: test
14
- Requires-Dist: pytest>=8; extra == "test"
15
- Requires-Dist: build>=1; extra == "test"
16
- Requires-Dist: setuptools>=77; extra == "test"
17
- Requires-Dist: wheel; extra == "test"
18
- Requires-Dist: packaging; extra == "test"
19
- Dynamic: license-file
20
-
21
1
  # PostgreSQL persistence for Prosaic Harness
22
2
 
23
3
  Run on worker A, inspect/resume on worker B, without a shared writable run
@@ -316,3 +296,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
316
296
  with `--require-postgres`. Missing infrastructure or any skipped required case
317
297
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
318
298
  before claiming the conversation candidate is ready for production.
299
+
300
+ ## Optional context storage contract
301
+
302
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
303
+ and v2 records, and preserves existing documents. New database initialization
304
+ creates contract 2. Existing contract-1 databases require an explicit migration;
305
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
306
+ then use migration-owner credentials:
307
+
308
+ ```sh
309
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
310
+ ```
311
+
312
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
313
+ The migration is locked, transactional and repeatable; restricted runtime roles
314
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
315
+ Keep migration-owner credentials separate from runtime credentials. Enable host
316
+ context policies only after Runtime, Harness and store advertise the capability.
317
+ No production token counter is bundled or certified by this storage adapter.
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "b3-prosaic-harness-postgres"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Explicit PostgreSQL run and conversation storage for Prosaic Harness"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE", "NOTICE"]
13
- dependencies = ["b3-prosaic-harness>=0.8,<0.9", "psycopg[binary]>=3.2.3,<4", "psycopg-pool>=3.2,<4"]
13
+ dependencies = ["b3-prosaic-harness>=0.9,<0.10", "psycopg[binary]>=3.2.3,<4", "psycopg-pool>=3.2,<4"]
14
14
 
15
15
  [project.optional-dependencies]
16
16
  test = ["pytest>=8", "build>=1", "setuptools>=77", "wheel", "packaging"]
@@ -1,3 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: b3-prosaic-harness-postgres
3
+ Version: 0.4.0
4
+ Summary: Explicit PostgreSQL run and conversation storage for Prosaic Harness
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Requires-Dist: b3-prosaic-harness<0.10,>=0.9
11
+ Requires-Dist: psycopg[binary]<4,>=3.2.3
12
+ Requires-Dist: psycopg-pool<4,>=3.2
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest>=8; extra == "test"
15
+ Requires-Dist: build>=1; extra == "test"
16
+ Requires-Dist: setuptools>=77; extra == "test"
17
+ Requires-Dist: wheel; extra == "test"
18
+ Requires-Dist: packaging; extra == "test"
19
+ Dynamic: license-file
20
+
1
21
  # PostgreSQL persistence for Prosaic Harness
2
22
 
3
23
  Run on worker A, inspect/resume on worker B, without a shared writable run
@@ -296,3 +316,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
296
316
  with `--require-postgres`. Missing infrastructure or any skipped required case
297
317
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
298
318
  before claiming the conversation candidate is ready for production.
319
+
320
+ ## Optional context storage contract
321
+
322
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
323
+ and v2 records, and preserves existing documents. New database initialization
324
+ creates contract 2. Existing contract-1 databases require an explicit migration;
325
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
326
+ then use migration-owner credentials:
327
+
328
+ ```sh
329
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
330
+ ```
331
+
332
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
333
+ The migration is locked, transactional and repeatable; restricted runtime roles
334
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
335
+ Keep migration-owner credentials separate from runtime credentials. Enable host
336
+ context policies only after Runtime, Harness and store advertise the capability.
337
+ No production token counter is bundled or certified by this storage adapter.
@@ -31,6 +31,7 @@ tests/test_recovery.py
31
31
  tests/test_registration.py
32
32
  tests/test_routing.py
33
33
  tests/test_schema.py
34
+ tests/test_session_context.py
34
35
  tests/test_session_fencing.py
35
36
  tests/test_session_recovery.py
36
37
  tests/test_session_schema.py
@@ -1,4 +1,4 @@
1
- b3-prosaic-harness<0.9,>=0.8
1
+ b3-prosaic-harness<0.10,>=0.9
2
2
  psycopg[binary]<4,>=3.2.3
3
3
  psycopg-pool<4,>=3.2
4
4
 
@@ -1,5 +1,5 @@
1
1
  """Separate PostgreSQL adapter for Prosaic Harness."""
2
- __version__ = '0.3.0'
2
+ __version__ = '0.4.0'
3
3
 
4
4
  from .store import PostgresRunStore
5
5
  from .session_store import PostgresSessionStore
@@ -44,13 +44,19 @@ def check_write(store):
44
44
  def main(argv=None):
45
45
  parser = argparse.ArgumentParser(description='Initialize/check an existing Harness database')
46
46
  commands = parser.add_subparsers(dest='command', required=True)
47
- for name in ('init', 'doctor'):
47
+ for name in ('init', 'doctor', 'migrate-conversations'):
48
48
  command = commands.add_parser(name)
49
49
  command.add_argument('--dsn-env', required=True, help='name of the DSN environment variable, never its value')
50
50
  if name == 'doctor':
51
51
  command.add_argument('--check-write', action='store_true', help='explicit disposable write test; DELETE credentials required')
52
52
  args = parser.parse_args(argv)
53
53
  try:
54
+ if args.command == 'migrate-conversations':
55
+ from .session_store import PostgresSessionStore
56
+ with PostgresSessionStore.from_env(args.dsn_env, namespace='conversation-migration') as store:
57
+ store.migrate_context_storage()
58
+ print(json.dumps({'migrated': True, 'conversation_storage_contract': 2}))
59
+ return 0
54
60
  namespace = 'prosaic-diagnostic-' + uuid.uuid4().hex
55
61
  with PostgresRunStore.from_env(args.dsn_env, namespace=namespace) as store:
56
62
  if args.command == 'init':
@@ -1,7 +1,7 @@
1
1
  """Independent, explicit version-1 conversation storage and readiness."""
2
2
  from prosaic_harness import StoreUninitialized, StoreIncompatible
3
3
 
4
- SCHEMA_VERSION = STORAGE_CONTRACT_VERSION = 1
4
+ SCHEMA_VERSION = STORAGE_CONTRACT_VERSION = 2
5
5
  MIGRATION_LOCK = 0x50524348415401
6
6
  SCHEMA = 'prosaic_harness_conversations'
7
7
 
@@ -33,7 +33,7 @@ CREATE TABLE prosaic_harness_conversations.receipts (
33
33
  PRIMARY KEY(namespace, session_id, invocation_id),
34
34
  FOREIGN KEY(namespace, session_id, scope_identity) REFERENCES
35
35
  prosaic_harness_conversations.sessions(namespace, session_id, scope_identity));
36
- INSERT INTO prosaic_harness_conversations.schema_version VALUES(TRUE, 1, 1);
36
+ INSERT INTO prosaic_harness_conversations.schema_version VALUES(TRUE, 2, 2);
37
37
  """
38
38
 
39
39
  COLUMNS = {
@@ -69,7 +69,7 @@ CONSTRAINTS = {
69
69
  }
70
70
 
71
71
 
72
- async def ready_on_connection(conn):
72
+ async def ready_on_connection(conn, *, expected_versions=None):
73
73
  namespace = await (await conn.execute("SELECT oid,pg_catalog.has_schema_privilege(oid,'USAGE'), "
74
74
  "pg_catalog.pg_has_role(nspowner,'USAGE') "
75
75
  "FROM pg_catalog.pg_namespace WHERE nspname=%s", (SCHEMA,))).fetchone()
@@ -154,7 +154,7 @@ async def ready_on_connection(conn):
154
154
  raise StoreIncompatible('use least-privilege conversation runtime grants')
155
155
  versions = await (await conn.execute('SELECT schema_version,storage_contract_version '
156
156
  'FROM prosaic_harness_conversations.schema_version')).fetchall()
157
- if versions != [(SCHEMA_VERSION, STORAGE_CONTRACT_VERSION)]:
157
+ if versions != [expected_versions or (SCHEMA_VERSION, STORAGE_CONTRACT_VERSION)]:
158
158
  raise StoreIncompatible('conversation schema version is incompatible; no automatic migration')
159
159
 
160
160
 
@@ -183,3 +183,30 @@ def initialize(transport):
183
183
  await conn.execute(DDL)
184
184
  await ready_on_connection(conn)
185
185
  transport.call(migrate, timeout_s=30)
186
+
187
+
188
+ def migrate_context_storage(transport):
189
+ async def migrate(conn):
190
+ await conn.execute("SELECT pg_catalog.set_config('lock_timeout','25000ms',true), "
191
+ "pg_catalog.set_config('statement_timeout','28000ms',true)")
192
+ await conn.execute('SELECT pg_catalog.pg_advisory_xact_lock(%s)', (MIGRATION_LOCK,))
193
+ namespace = await (await conn.execute("SELECT pg_catalog.pg_has_role(nspowner,'USAGE') "
194
+ 'FROM pg_catalog.pg_namespace WHERE nspname=%s', (SCHEMA,))).fetchone()
195
+ if namespace is None:
196
+ raise StoreUninitialized('initialize conversation storage explicitly first')
197
+ if not namespace[0]:
198
+ raise StoreIncompatible('migration role must own conversation schema')
199
+ versions = await (await conn.execute('SELECT schema_version,storage_contract_version '
200
+ 'FROM prosaic_harness_conversations.schema_version')).fetchall()
201
+ if versions == [(2, 2)]:
202
+ await ready_on_connection(conn)
203
+ return
204
+ if versions != [(1, 1)]:
205
+ raise StoreIncompatible('conversation migration requires intact version-1 storage')
206
+ await ready_on_connection(conn, expected_versions=(1, 1))
207
+ cursor = await conn.execute('UPDATE prosaic_harness_conversations.schema_version '
208
+ 'SET schema_version=2,storage_contract_version=2 WHERE singleton IS TRUE')
209
+ if cursor.rowcount != 1:
210
+ raise StoreIncompatible('conversation version metadata is incompatible')
211
+ await ready_on_connection(conn)
212
+ transport.call(migrate, timeout_s=30)
@@ -60,6 +60,13 @@ class PostgresSessionStore:
60
60
 
61
61
  def check_ready(self):
62
62
  session_schema.check_ready(self._transport)
63
+ @property
64
+ def capabilities(self):
65
+ self.check_ready()
66
+ return frozenset({'context_assembly_v1'})
67
+
68
+ def migrate_context_storage(self):
69
+ session_schema.migrate_context_storage(self._transport)
63
70
 
64
71
  def _scope(self, scope):
65
72
  if type(scope) is not ConversationScope or scope.namespace != self.namespace:
@@ -40,7 +40,7 @@ def candidate_wheels(root):
40
40
  spec.loader.exec_module(release)
41
41
  release.verify_upstream(Path(receipt_directory).resolve(strict=True), wheelhouse)
42
42
  return [wheelhouse / 'b3_prosaic-0.4.1-py3-none-any.whl',
43
- wheelhouse / 'b3_prosaic_runtime-0.9.0-py3-none-any.whl']
43
+ wheelhouse / 'b3_prosaic_runtime-0.9.4-py3-none-any.whl']
44
44
 
45
45
 
46
46
  class LocalCluster:
@@ -177,12 +177,12 @@ class LocalCluster:
177
177
  set -eu
178
178
  apt-get update -qq
179
179
  python -m venv /tmp/core
180
- /tmp/core/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.8.0-py3-none-any.whl >/dev/null
180
+ /tmp/core/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.0-py3-none-any.whl >/dev/null
181
181
  /tmp/core/bin/python -I -c 'import importlib.util,prosaic_harness; assert importlib.util.find_spec("psycopg") is None; assert importlib.util.find_spec("prosaic_harness_postgres") is None'
182
182
  /tmp/core/bin/python -I /fixture/workflow_factory_smoke.py
183
183
  /tmp/core/bin/python -I /fixture/conversation_smoke.py
184
184
  python -m venv /tmp/both
185
- /tmp/both/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.8.0-py3-none-any.whl /adapter-wheels/b3_prosaic_harness_postgres-0.3.0-py3-none-any.whl >/dev/null
185
+ /tmp/both/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.0-py3-none-any.whl /adapter-wheels/b3_prosaic_harness_postgres-0.4.0-py3-none-any.whl >/dev/null
186
186
  /tmp/both/bin/python -I /fixture/linux_wheel_smoke.py
187
187
  '''
188
188
  if dependencies:
@@ -266,12 +266,13 @@ def main():
266
266
  cluster.stop()
267
267
  else:
268
268
  env = os.environ | {'HARNESS_TEST_DATABASE_URL': cluster.dsn,
269
- 'HARNESS_TEST_CONTAINER_ENGINE': cluster.engine, 'HARNESS_TEST_POSTGRES_VERSION': cluster.version}
269
+ 'HARNESS_TEST_CONTAINER_ENGINE': cluster.engine, 'HARNESS_TEST_POSTGRES_VERSION': cluster.version,
270
+ 'PROSAIC_LIFECYCLE_TEST_DSN': cluster.dsn}
270
271
  root = Path(__file__).resolve().parents[4]
271
272
  report = (['--junitxml=' + env['HARNESS_TEST_JUNIT_XML']]
272
273
  if env.get('HARNESS_TEST_JUNIT_XML') else [])
273
274
  raise SystemExit(subprocess.run([sys.executable, '-m', 'pytest', '-q',
274
- 'adapters/postgres/tests', '--require-postgres', *report],
275
+ 'adapters/postgres/tests', 'tests/test_conversation_postgres_lifecycle.py', '--require-postgres', *report],
275
276
  cwd=root, env=env, timeout=600).returncode)
276
277
 
277
278
 
@@ -38,3 +38,29 @@ def receipt(state, *, invocation_id='b' * 32, failure=None, uncertain=False):
38
38
  result = TurnResult(Result(1 if failure else 0, '' if failure else 'Done', failure or ''),
39
39
  completed, evidence)
40
40
  return make_receipt(state, invocation_id, result)
41
+
42
+
43
+ def context_document(scope, *, session_id='a'*32):
44
+ from prosaic_runtime import (ContextPolicy, TokenCounterDescriptor,
45
+ TokenCounterRegistry, TokenCounterRegistration)
46
+ base = bindings()
47
+ class Counter:
48
+ def count_tokens(self, request): return 10
49
+ descriptor = TokenCounterDescriptor('fixture-v1', 'openai-compatible',
50
+ 'synthetic', 'openai-chat-v1', 4096, '1', '0'*64, '1'*64,
51
+ 'exact', 'Synthetic fixed fixture grammar only.')
52
+ counters = TokenCounterRegistry({'fixture-v1': TokenCounterRegistration(descriptor, Counter())})
53
+ policy = ContextPolicy('fullHistory', 4096, 128, 0, 'fixture-v1')
54
+ binding = ConversationBindings(base.agent, version='context-v1', config=base.config,
55
+ policy=base.policy, run_policy=base.run_policy,
56
+ context_policy=policy, token_counters=counters)
57
+ state = new_session(session_id, scope, binding)
58
+ prepared = binding.runtime().prepare_turn(binding.agent, message='Hello ž',
59
+ policy=binding.run_policy, limits=binding.turn_limits,
60
+ context_policy=policy, token_counters=counters)
61
+ state = reserve_attempt(state, message_id='message', message='Hello ž',
62
+ invocation_id='b'*32, context_identity=prepared.context_identity,
63
+ encoding_version=prepared.encoding_version, input_bytes=prepared.input_bytes,
64
+ provider_allowance=2, tool_allowance=0, context_selection=prepared.context_selection,
65
+ context_report=prepared.context_report, context_work=prepared.context_work)
66
+ return state, prepared
@@ -13,8 +13,8 @@ import pytest
13
13
  import yaml
14
14
 
15
15
  ROOT = Path(__file__).resolve().parents[3]
16
- CORE = ROOT / 'dist/b3_prosaic_harness-0.8.0-py3-none-any.whl'
17
- ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.3.0-py3-none-any.whl'
16
+ CORE = ROOT / 'dist/b3_prosaic_harness-0.9.0-py3-none-any.whl'
17
+ ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.4.0-py3-none-any.whl'
18
18
 
19
19
  def test_owned_fixture_uses_free_explicit_subnet_when_default_pool_is_exhausted():
20
20
  from support.local_cluster import owned_subnet
@@ -73,13 +73,13 @@ def metadata(path):
73
73
  def test_wheel_contents_and_dependency_boundaries():
74
74
  core, names = metadata(CORE)
75
75
  assert core['Name'] == 'b3-prosaic-harness'
76
- assert core['Version'] == '0.8.0'
76
+ assert core['Version'] == '0.9.0'
77
77
  assert not any(name.startswith('prosaic_harness_postgres/') for name in names)
78
78
  assert not any('psycopg' in value.lower() for value in core.get_all('Requires-Dist'))
79
79
  adapter, names = metadata(ADAPTER)
80
80
  assert adapter['Name'] == 'b3-prosaic-harness-postgres'
81
- assert adapter['Version'] == '0.3.0'
82
- assert any(value.startswith('b3-prosaic-harness<0.9,>=0.8') for value in adapter.get_all('Requires-Dist'))
81
+ assert adapter['Version'] == '0.4.0'
82
+ assert any(value.startswith('b3-prosaic-harness<0.10,>=0.9') for value in adapter.get_all('Requires-Dist'))
83
83
  assert 'prosaic_harness_postgres/cli.py' in names
84
84
  assert any(value.startswith('psycopg[binary]<4,>=3.2.3')
85
85
  for value in adapter.get_all('Requires-Dist'))
@@ -184,7 +184,7 @@ def curated_candidates(release_tool, tmp_path):
184
184
  candidates = []
185
185
  for repo, expected in (
186
186
  ('B3Cognition/prosaic', {'b3-prosaic': '0.4.1'}),
187
- ('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.0',
187
+ ('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.4',
188
188
  'b3-prosaic-runtime-postgres': '0.3.0'})):
189
189
  artifacts = []
190
190
  for name, version in expected.items():
@@ -252,4 +252,4 @@ def test_container_mounts_verified_dependency_bytes_read_only(
252
252
  create = calls[0]
253
253
  assert str(wheelhouse) + ':/candidate-wheels:ro' in create
254
254
  assert 'PIP_FIND_LINKS=/candidate-wheels' in create[-1]
255
- assert '/candidate-wheels/b3_prosaic_runtime-0.9.0-py3-none-any.whl' in create[-1]
255
+ assert '/candidate-wheels/b3_prosaic_runtime-0.9.4-py3-none-any.whl' in create[-1]
@@ -0,0 +1,112 @@
1
+ """Owner-only explicit v1->v2 migration; real PostgreSQL transactional evidence."""
2
+ import pytest
3
+ import psycopg
4
+ from prosaic_harness import StoreIncompatible, ConversationScope
5
+ from prosaic_harness.session_contracts import new_session
6
+ import prosaic_harness_postgres as pg
7
+ from prosaic_harness_postgres import session_schema
8
+ from test_session_schema import grant_conversations
9
+ from support.session_documents import bindings
10
+ from dataclasses import replace
11
+
12
+
13
+ def legacy_storage(db_admin_dsn):
14
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
15
+ owner.initialize()
16
+ # Table/role contract is identical in v1; only explicit contract metadata changes.
17
+ with psycopg.connect(db_admin_dsn) as connection:
18
+ connection.execute('UPDATE prosaic_harness_conversations.schema_version '
19
+ 'SET schema_version=1,storage_contract_version=1')
20
+
21
+
22
+ def test_owner_migration_is_explicit_repeatable_and_keeps_v1_records(db_admin_dsn):
23
+ scope = ConversationScope('context-test', 'sandbox', 'app', 'tenant', 'actor')
24
+ state = new_session('a'*32, scope, bindings())
25
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
26
+ owner.initialize()
27
+ with owner.lease('a'*32, scope=scope) as lease:
28
+ owner.create_session('a'*32, state, lease, scope=scope)
29
+ before = owner.load_session('a'*32, scope=scope)
30
+ with psycopg.connect(db_admin_dsn) as connection:
31
+ connection.execute('UPDATE prosaic_harness_conversations.schema_version '
32
+ 'SET schema_version=1,storage_contract_version=1')
33
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
34
+ with pytest.raises(StoreIncompatible): owner.check_ready()
35
+ with pytest.raises(StoreIncompatible): owner.initialize()
36
+ owner.migrate_context_storage()
37
+ owner.migrate_context_storage()
38
+ assert 'context_assembly_v1' in owner.capabilities
39
+ assert owner.load_session('a'*32, scope=scope) == before
40
+ with pytest.raises(StoreIncompatible):
41
+ owner._transport.call(lambda connection:
42
+ session_schema.ready_on_connection(connection, expected_versions=(1, 1)))
43
+
44
+
45
+ def test_runtime_role_cannot_migrate_or_gain_owner_privileges(db_admin_dsn, runtime_dsn):
46
+ legacy_storage(db_admin_dsn)
47
+ grant_conversations(db_admin_dsn, runtime_dsn)
48
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as runtime:
49
+ with pytest.raises(StoreIncompatible): runtime.migrate_context_storage()
50
+ with psycopg.connect(db_admin_dsn) as connection:
51
+ assert connection.execute('SELECT schema_version,storage_contract_version '
52
+ 'FROM prosaic_harness_conversations.schema_version').fetchone() == (1, 1)
53
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
54
+ owner.migrate_context_storage()
55
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as runtime:
56
+ assert 'context_assembly_v1' in runtime.capabilities
57
+
58
+
59
+ def test_v2_checkpoint_is_fenced_and_owner_filtered(db_admin_dsn, runtime_dsn):
60
+ from prosaic_runtime import ContextWorkState
61
+ from prosaic_harness import LeaseLost, SessionNotFound
62
+ from prosaic_harness.session_context import checkpoint_context_work
63
+ from support.session_documents import context_document
64
+ scope = ConversationScope('context-test', 'sandbox', 'app', 'tenant', 'actor')
65
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
66
+ owner.initialize()
67
+ grant_conversations(db_admin_dsn, runtime_dsn)
68
+ state, prepared = context_document(scope)
69
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as first, \
70
+ pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as replica:
71
+ with first.lease('a'*32, scope=scope) as lease:
72
+ revision = first.create_session('a'*32, state, lease, scope=scope)
73
+ before = prepared.context_work.to_dict()
74
+ work = ContextWorkState.from_dict({**before, 'checkpointSequence': 1,
75
+ 'reservedActiveMicros': 2_000_000 - before['activeAssemblyMicros']})
76
+ changed = checkpoint_context_work(state, 'b'*32, work)
77
+ current = first.save_session('a'*32, changed, revision, lease, scope=scope)
78
+ assert replica.load_session('a'*32, scope=scope).state == changed
79
+ with pytest.raises(SessionNotFound):
80
+ replica.load_session('a'*32, scope=replace(scope, actor='foreign'))
81
+ with psycopg.connect(db_admin_dsn) as admin:
82
+ admin.execute('UPDATE prosaic_harness_conversations.leases '
83
+ 'SET generation=generation+1 WHERE namespace=%s AND session_id=%s',
84
+ ('context-test', 'a'*32))
85
+ with pytest.raises(LeaseLost):
86
+ first.save_session('a'*32, changed, current, lease, scope=scope)
87
+ def test_failed_postupdate_validation_rolls_back_metadata(db_admin_dsn, monkeypatch):
88
+ legacy_storage(db_admin_dsn)
89
+ original = session_schema.ready_on_connection
90
+ async def reject_v2(connection, **kwargs):
91
+ row = await (await connection.execute('SELECT schema_version FROM '
92
+ 'prosaic_harness_conversations.schema_version')).fetchone()
93
+ if row == (2,): raise StoreIncompatible('synthetic postupdate validation failure')
94
+ return await original(connection, **kwargs)
95
+ monkeypatch.setattr(session_schema, 'ready_on_connection', reject_v2)
96
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
97
+ with pytest.raises(StoreIncompatible): owner.migrate_context_storage()
98
+ with psycopg.connect(db_admin_dsn) as connection:
99
+ assert connection.execute('SELECT schema_version,storage_contract_version '
100
+ 'FROM prosaic_harness_conversations.schema_version').fetchone() == (1, 1)
101
+
102
+
103
+ def test_operator_cli_migrates_without_exposing_connection_details(db_admin_dsn, monkeypatch, capsys):
104
+ import json
105
+ from prosaic_harness_postgres.cli import main
106
+ legacy_storage(db_admin_dsn)
107
+ monkeypatch.setenv('CONTEXT_TEST_DSN', db_admin_dsn)
108
+ assert main(['migrate-conversations', '--dsn-env', 'CONTEXT_TEST_DSN']) == 0
109
+ output = capsys.readouterr()
110
+ assert json.loads(output.out) == {'migrated': True, 'conversation_storage_contract': 2}
111
+ assert output.err == ''
112
+ assert db_admin_dsn not in output.out