b3-prosaic-harness-postgres 0.3.0__tar.gz → 0.4.1__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.
- {b3_prosaic_harness_postgres-0.3.0/src/b3_prosaic_harness_postgres.egg-info → b3_prosaic_harness_postgres-0.4.1}/PKG-INFO +50 -9
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/README.md +48 -7
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/pyproject.toml +2 -2
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1/src/b3_prosaic_harness_postgres.egg-info}/PKG-INFO +50 -9
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/SOURCES.txt +2 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/requires.txt +1 -1
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/__init__.py +1 -1
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/cli.py +10 -2
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_schema.py +31 -4
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_store.py +9 -2
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/store.py +2 -2
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/transport.py +7 -2
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/local_cluster.py +6 -5
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/session_documents.py +26 -0
- b3_prosaic_harness_postgres-0.4.1/tests/test_durability_profiles.py +96 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_packaging.py +9 -8
- b3_prosaic_harness_postgres-0.4.1/tests/test_session_context.py +112 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/LICENSE +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/MANIFEST.in +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/NOTICE +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/setup.cfg +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/dependency_links.txt +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/entry_points.txt +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/top_level.txt +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/leases.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/schema.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_leases.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/conftest.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/linux_wheel_smoke.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/model_server.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/session_workers.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/tcp_proxy.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/workers.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_cli.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_durability.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_engine.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_examples.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_leases.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_local_cluster.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_native_factory.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_recovery.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_registration.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_routing.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_schema.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_fencing.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_recovery.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_schema.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_store.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_workers.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_store.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_transport.py +0 -0
- {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/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
|
+
Version: 0.4.1
|
|
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.
|
|
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
|
|
@@ -24,13 +24,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
|
|
|
24
24
|
directory. Core remains database-free; install this separate adapter only when
|
|
25
25
|
you need shared durable state. The API is synchronous; a private I/O thread
|
|
26
26
|
bounds network waits. Model execution holds no database connection/transaction.
|
|
27
|
-
The
|
|
28
|
-
`b3-prosaic-harness>=0.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
|
|
28
|
+
requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
|
|
29
|
+
core Harness users. Imports and CLI names are unchanged.
|
|
30
|
+
|
|
31
|
+
## Durability profiles
|
|
32
|
+
|
|
33
|
+
The default `durability_profile="postgresql"` requires `full_page_writes=on`.
|
|
34
|
+
An operator who has independently verified an Amazon Aurora PostgreSQL cluster
|
|
35
|
+
may explicitly select `durability_profile="aurora-postgresql"` on both
|
|
36
|
+
`PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
|
|
37
|
+
`full_page_writes=off` under Aurora's log-based storage contract. It does not
|
|
38
|
+
weaken fsync, synchronous commit, writable-primary, logged-table, lease or
|
|
39
|
+
unknown-commit checks. The guard runs for every operation and connection reuse.
|
|
40
|
+
Invalid profiles fail before connecting. This profile is an operator assertion,
|
|
41
|
+
not engine autodetection or proof that arbitrary storage is safe. NEVER select
|
|
42
|
+
it for ordinary PostgreSQL just to suppress a readiness failure.
|
|
43
|
+
|
|
44
|
+
For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
|
|
45
|
+
Migration and runtime must select the same profile. Verify the actual engine and
|
|
46
|
+
version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
|
|
47
|
+
Capture a failed SQL probe's actual error and inspect its schema/search path;
|
|
48
|
+
NEVER convert an unavailable probe into permission to bypass durability checks.
|
|
49
|
+
No AWS credentials or model calls are needed by the adapter.
|
|
50
|
+
|
|
51
|
+
AWS describes the storage contract in
|
|
52
|
+
[its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
|
|
53
|
+
Native PostgreSQL tests qualify the policy and all existing recovery behavior;
|
|
54
|
+
they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
|
|
55
|
+
persistence/restart/failover qualification before enabling production work.
|
|
34
56
|
|
|
35
57
|
## 1. Install and supply an existing database
|
|
36
58
|
|
|
@@ -316,3 +338,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
|
|
|
316
338
|
with `--require-postgres`. Missing infrastructure or any skipped required case
|
|
317
339
|
fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
|
|
318
340
|
before claiming the conversation candidate is ready for production.
|
|
341
|
+
|
|
342
|
+
## Optional context storage contract
|
|
343
|
+
|
|
344
|
+
Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
|
|
345
|
+
and v2 records, and preserves existing documents. New database initialization
|
|
346
|
+
creates contract 2. Existing contract-1 databases require an explicit migration;
|
|
347
|
+
runtime readiness never performs it. Drain old writers, deploy upgraded workers,
|
|
348
|
+
then use migration-owner credentials:
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
|
|
355
|
+
The migration is locked, transactional and repeatable; restricted runtime roles
|
|
356
|
+
cannot update schema metadata. Old workers reject the upgraded storage contract.
|
|
357
|
+
Keep migration-owner credentials separate from runtime credentials. Enable host
|
|
358
|
+
context policies only after Runtime, Harness and store advertise the capability.
|
|
359
|
+
No production token counter is bundled or certified by this storage adapter.
|
|
@@ -4,13 +4,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
|
|
|
4
4
|
directory. Core remains database-free; install this separate adapter only when
|
|
5
5
|
you need shared durable state. The API is synchronous; a private I/O thread
|
|
6
6
|
bounds network waits. Model execution holds no database connection/transaction.
|
|
7
|
-
The
|
|
8
|
-
`b3-prosaic-harness>=0.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
|
|
8
|
+
requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
|
|
9
|
+
core Harness users. Imports and CLI names are unchanged.
|
|
10
|
+
|
|
11
|
+
## Durability profiles
|
|
12
|
+
|
|
13
|
+
The default `durability_profile="postgresql"` requires `full_page_writes=on`.
|
|
14
|
+
An operator who has independently verified an Amazon Aurora PostgreSQL cluster
|
|
15
|
+
may explicitly select `durability_profile="aurora-postgresql"` on both
|
|
16
|
+
`PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
|
|
17
|
+
`full_page_writes=off` under Aurora's log-based storage contract. It does not
|
|
18
|
+
weaken fsync, synchronous commit, writable-primary, logged-table, lease or
|
|
19
|
+
unknown-commit checks. The guard runs for every operation and connection reuse.
|
|
20
|
+
Invalid profiles fail before connecting. This profile is an operator assertion,
|
|
21
|
+
not engine autodetection or proof that arbitrary storage is safe. NEVER select
|
|
22
|
+
it for ordinary PostgreSQL just to suppress a readiness failure.
|
|
23
|
+
|
|
24
|
+
For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
|
|
25
|
+
Migration and runtime must select the same profile. Verify the actual engine and
|
|
26
|
+
version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
|
|
27
|
+
Capture a failed SQL probe's actual error and inspect its schema/search path;
|
|
28
|
+
NEVER convert an unavailable probe into permission to bypass durability checks.
|
|
29
|
+
No AWS credentials or model calls are needed by the adapter.
|
|
30
|
+
|
|
31
|
+
AWS describes the storage contract in
|
|
32
|
+
[its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
|
|
33
|
+
Native PostgreSQL tests qualify the policy and all existing recovery behavior;
|
|
34
|
+
they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
|
|
35
|
+
persistence/restart/failover qualification before enabling production work.
|
|
14
36
|
|
|
15
37
|
## 1. Install and supply an existing database
|
|
16
38
|
|
|
@@ -296,3 +318,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
|
|
|
296
318
|
with `--require-postgres`. Missing infrastructure or any skipped required case
|
|
297
319
|
fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
|
|
298
320
|
before claiming the conversation candidate is ready for production.
|
|
321
|
+
|
|
322
|
+
## Optional context storage contract
|
|
323
|
+
|
|
324
|
+
Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
|
|
325
|
+
and v2 records, and preserves existing documents. New database initialization
|
|
326
|
+
creates contract 2. Existing contract-1 databases require an explicit migration;
|
|
327
|
+
runtime readiness never performs it. Drain old writers, deploy upgraded workers,
|
|
328
|
+
then use migration-owner credentials:
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
|
|
335
|
+
The migration is locked, transactional and repeatable; restricted runtime roles
|
|
336
|
+
cannot update schema metadata. Old workers reject the upgraded storage contract.
|
|
337
|
+
Keep migration-owner credentials separate from runtime credentials. Enable host
|
|
338
|
+
context policies only after Runtime, Harness and store advertise the capability.
|
|
339
|
+
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.
|
|
7
|
+
version = "0.4.1"
|
|
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.
|
|
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,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b3-prosaic-harness-postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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.
|
|
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
|
|
@@ -24,13 +24,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
|
|
|
24
24
|
directory. Core remains database-free; install this separate adapter only when
|
|
25
25
|
you need shared durable state. The API is synchronous; a private I/O thread
|
|
26
26
|
bounds network waits. Model execution holds no database connection/transaction.
|
|
27
|
-
The
|
|
28
|
-
`b3-prosaic-harness>=0.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
|
|
28
|
+
requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
|
|
29
|
+
core Harness users. Imports and CLI names are unchanged.
|
|
30
|
+
|
|
31
|
+
## Durability profiles
|
|
32
|
+
|
|
33
|
+
The default `durability_profile="postgresql"` requires `full_page_writes=on`.
|
|
34
|
+
An operator who has independently verified an Amazon Aurora PostgreSQL cluster
|
|
35
|
+
may explicitly select `durability_profile="aurora-postgresql"` on both
|
|
36
|
+
`PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
|
|
37
|
+
`full_page_writes=off` under Aurora's log-based storage contract. It does not
|
|
38
|
+
weaken fsync, synchronous commit, writable-primary, logged-table, lease or
|
|
39
|
+
unknown-commit checks. The guard runs for every operation and connection reuse.
|
|
40
|
+
Invalid profiles fail before connecting. This profile is an operator assertion,
|
|
41
|
+
not engine autodetection or proof that arbitrary storage is safe. NEVER select
|
|
42
|
+
it for ordinary PostgreSQL just to suppress a readiness failure.
|
|
43
|
+
|
|
44
|
+
For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
|
|
45
|
+
Migration and runtime must select the same profile. Verify the actual engine and
|
|
46
|
+
version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
|
|
47
|
+
Capture a failed SQL probe's actual error and inspect its schema/search path;
|
|
48
|
+
NEVER convert an unavailable probe into permission to bypass durability checks.
|
|
49
|
+
No AWS credentials or model calls are needed by the adapter.
|
|
50
|
+
|
|
51
|
+
AWS describes the storage contract in
|
|
52
|
+
[its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
|
|
53
|
+
Native PostgreSQL tests qualify the policy and all existing recovery behavior;
|
|
54
|
+
they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
|
|
55
|
+
persistence/restart/failover qualification before enabling production work.
|
|
34
56
|
|
|
35
57
|
## 1. Install and supply an existing database
|
|
36
58
|
|
|
@@ -316,3 +338,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
|
|
|
316
338
|
with `--require-postgres`. Missing infrastructure or any skipped required case
|
|
317
339
|
fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
|
|
318
340
|
before claiming the conversation candidate is ready for production.
|
|
341
|
+
|
|
342
|
+
## Optional context storage contract
|
|
343
|
+
|
|
344
|
+
Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
|
|
345
|
+
and v2 records, and preserves existing documents. New database initialization
|
|
346
|
+
creates contract 2. Existing contract-1 databases require an explicit migration;
|
|
347
|
+
runtime readiness never performs it. Drain old writers, deploy upgraded workers,
|
|
348
|
+
then use migration-owner credentials:
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
|
|
355
|
+
The migration is locked, transactional and repeatable; restricted runtime roles
|
|
356
|
+
cannot update schema metadata. Old workers reject the upgraded storage contract.
|
|
357
|
+
Keep migration-owner credentials separate from runtime credentials. Enable host
|
|
358
|
+
context policies only after Runtime, Harness and store advertise the capability.
|
|
359
|
+
No production token counter is bundled or certified by this storage adapter.
|
|
@@ -21,6 +21,7 @@ src/prosaic_harness_postgres/transport.py
|
|
|
21
21
|
tests/conftest.py
|
|
22
22
|
tests/test_cli.py
|
|
23
23
|
tests/test_durability.py
|
|
24
|
+
tests/test_durability_profiles.py
|
|
24
25
|
tests/test_engine.py
|
|
25
26
|
tests/test_examples.py
|
|
26
27
|
tests/test_leases.py
|
|
@@ -31,6 +32,7 @@ tests/test_recovery.py
|
|
|
31
32
|
tests/test_registration.py
|
|
32
33
|
tests/test_routing.py
|
|
33
34
|
tests/test_schema.py
|
|
35
|
+
tests/test_session_context.py
|
|
34
36
|
tests/test_session_fencing.py
|
|
35
37
|
tests/test_session_recovery.py
|
|
36
38
|
tests/test_session_schema.py
|
|
@@ -44,15 +44,23 @@ 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
|
+
command.add_argument('--durability-profile', choices=['postgresql', 'aurora-postgresql'],
|
|
51
|
+
default='postgresql', help='explicit operator-verified database storage contract')
|
|
50
52
|
if name == 'doctor':
|
|
51
53
|
command.add_argument('--check-write', action='store_true', help='explicit disposable write test; DELETE credentials required')
|
|
52
54
|
args = parser.parse_args(argv)
|
|
53
55
|
try:
|
|
56
|
+
if args.command == 'migrate-conversations':
|
|
57
|
+
from .session_store import PostgresSessionStore
|
|
58
|
+
with PostgresSessionStore.from_env(args.dsn_env, namespace='conversation-migration', durability_profile=args.durability_profile) as store:
|
|
59
|
+
store.migrate_context_storage()
|
|
60
|
+
print(json.dumps({'migrated': True, 'conversation_storage_contract': 2}))
|
|
61
|
+
return 0
|
|
54
62
|
namespace = 'prosaic-diagnostic-' + uuid.uuid4().hex
|
|
55
|
-
with PostgresRunStore.from_env(args.dsn_env, namespace=namespace) as store:
|
|
63
|
+
with PostgresRunStore.from_env(args.dsn_env, namespace=namespace, durability_profile=args.durability_profile) as store:
|
|
56
64
|
if args.command == 'init':
|
|
57
65
|
store.initialize()
|
|
58
66
|
result = {'initialized': True, 'schema_version': 1}
|
|
@@ -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 =
|
|
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,
|
|
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)
|
|
@@ -33,7 +33,7 @@ class PostgresSessionStore:
|
|
|
33
33
|
|
|
34
34
|
def __init__(self, dsn, *, namespace, lease_s=60, renew_s=15,
|
|
35
35
|
operation_timeout_s=10, pool_timeout_s=5, connect_timeout_s=5,
|
|
36
|
-
lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4):
|
|
36
|
+
lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4, durability_profile="postgresql"):
|
|
37
37
|
self._namespace = validate_namespace(namespace)
|
|
38
38
|
positive(lease_s, 'lease_s')
|
|
39
39
|
positive(renew_s, 'renew_s')
|
|
@@ -49,7 +49,7 @@ class PostgresSessionStore:
|
|
|
49
49
|
self._transport = PostgresTransport(dsn, operation_timeout_s=operation_timeout_s,
|
|
50
50
|
pool_timeout_s=pool_timeout_s, connect_timeout_s=connect_timeout_s,
|
|
51
51
|
lock_timeout_s=lock_timeout_s, statement_timeout_s=statement_timeout_s,
|
|
52
|
-
pool_size=pool_size, max_active_leases=max_active_leases)
|
|
52
|
+
pool_size=pool_size, max_active_leases=max_active_leases, durability_profile=durability_profile)
|
|
53
53
|
|
|
54
54
|
@property
|
|
55
55
|
def namespace(self):
|
|
@@ -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:
|
|
@@ -29,7 +29,7 @@ class PostgresRunStore:
|
|
|
29
29
|
|
|
30
30
|
def __init__(self, dsn, *, namespace, lease_s=60, renew_s=15,
|
|
31
31
|
operation_timeout_s=10, pool_timeout_s=5, connect_timeout_s=5,
|
|
32
|
-
lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4):
|
|
32
|
+
lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4, durability_profile="postgresql"):
|
|
33
33
|
self.namespace = validate_namespace(namespace)
|
|
34
34
|
positive(lease_s, 'lease_s')
|
|
35
35
|
positive(renew_s, 'renew_s')
|
|
@@ -46,7 +46,7 @@ class PostgresRunStore:
|
|
|
46
46
|
self._transport = PostgresTransport(dsn, operation_timeout_s=operation_timeout_s,
|
|
47
47
|
pool_timeout_s=pool_timeout_s, connect_timeout_s=connect_timeout_s,
|
|
48
48
|
lock_timeout_s=lock_timeout_s, statement_timeout_s=statement_timeout_s,
|
|
49
|
-
pool_size=pool_size, max_active_leases=max_active_leases)
|
|
49
|
+
pool_size=pool_size, max_active_leases=max_active_leases, durability_profile=durability_profile)
|
|
50
50
|
|
|
51
51
|
def initialize(self):
|
|
52
52
|
schema.initialize(self._transport)
|
|
@@ -47,7 +47,10 @@ class _Operation:
|
|
|
47
47
|
class PostgresTransport:
|
|
48
48
|
def __init__(self, dsn, *, operation_timeout_s=10, pool_timeout_s=5,
|
|
49
49
|
connect_timeout_s=5, lock_timeout_s=1, statement_timeout_s=5,
|
|
50
|
-
pool_size=8, max_active_leases=4):
|
|
50
|
+
pool_size=8, max_active_leases=4, durability_profile="postgresql"):
|
|
51
|
+
if type(durability_profile) is not str or durability_profile not in {'postgresql', 'aurora-postgresql'}:
|
|
52
|
+
raise ValueError('durability_profile must be postgresql or aurora-postgresql')
|
|
53
|
+
self.durability_profile = durability_profile
|
|
51
54
|
for name, value in locals().copy().items():
|
|
52
55
|
if name.endswith('_s'):
|
|
53
56
|
positive(value, name)
|
|
@@ -138,7 +141,9 @@ class PostgresTransport:
|
|
|
138
141
|
pg_catalog.current_setting('transaction_read_only'),
|
|
139
142
|
pg_catalog.current_setting('fsync'), pg_catalog.current_setting('full_page_writes'),
|
|
140
143
|
pg_catalog.current_setting('synchronous_commit')""")).fetchone()
|
|
141
|
-
|
|
144
|
+
full_page_values = {'on', 'off'} if self.durability_profile == 'aurora-postgresql' else {'on'}
|
|
145
|
+
if (not row or row[0] or row[1:3] != ('off', 'on')
|
|
146
|
+
or row[3] not in full_page_values or row[4] not in {'on', 'remote_apply'}):
|
|
142
147
|
raise StoreIncompatible('require writable primary, logged durability and safe WAL settings')
|
|
143
148
|
|
|
144
149
|
async def _sql(self, operation, callback, renewal, budget):
|
|
@@ -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.
|
|
43
|
+
wheelhouse / 'b3_prosaic_runtime-0.9.7-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.
|
|
180
|
+
/tmp/core/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.4-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.
|
|
185
|
+
/tmp/both/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.4-py3-none-any.whl /adapter-wheels/b3_prosaic_harness_postgres-0.4.1-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
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""An explicit storage contract changes only the full-page-write guard."""
|
|
2
|
+
import asyncio
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from prosaic_harness import StoreIncompatible
|
|
7
|
+
from prosaic_harness_postgres.transport import PostgresTransport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize('profile', ['postgresql', 'aurora-postgresql'])
|
|
11
|
+
@pytest.mark.parametrize('row,accepted', [
|
|
12
|
+
((False, 'off', 'on', 'on', 'on'), True),
|
|
13
|
+
((False, 'off', 'on', 'off', 'on'), 'aurora'),
|
|
14
|
+
((False, 'off', 'off', 'off', 'on'), False),
|
|
15
|
+
((False, 'off', 'on', 'off', 'off'), False),
|
|
16
|
+
((True, 'off', 'on', 'off', 'on'), False),
|
|
17
|
+
((False, 'on', 'on', 'off', 'on'), False),
|
|
18
|
+
((False, 'off', 'on', 'on', 'remote_apply'), True),
|
|
19
|
+
((False, 'off', 'on', 'on', 'local'), False),
|
|
20
|
+
((False, 'off', 'on', 'invalid', 'on'), False),
|
|
21
|
+
(None, False),
|
|
22
|
+
])
|
|
23
|
+
def test_profile_changes_only_full_page_requirement(profile, row, accepted):
|
|
24
|
+
transport = object.__new__(PostgresTransport)
|
|
25
|
+
transport.durability_profile = profile
|
|
26
|
+
cursor = AsyncMock()
|
|
27
|
+
cursor.fetchone.return_value = row
|
|
28
|
+
connection = AsyncMock()
|
|
29
|
+
connection.execute.return_value = cursor
|
|
30
|
+
allowed = accepted is True or (accepted == 'aurora' and profile == 'aurora-postgresql')
|
|
31
|
+
if allowed:
|
|
32
|
+
asyncio.run(transport._guard(connection))
|
|
33
|
+
else:
|
|
34
|
+
with pytest.raises(StoreIncompatible):
|
|
35
|
+
asyncio.run(transport._guard(connection))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize('profile', ['', 'storage', 'Aurora', None, True])
|
|
39
|
+
def test_unknown_profile_rejected_before_connection(profile):
|
|
40
|
+
with pytest.raises(ValueError, match='durability_profile'):
|
|
41
|
+
PostgresTransport('unused', durability_profile=profile)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize('module_name,class_name', [('store', 'PostgresRunStore'), ('session_store', 'PostgresSessionStore')])
|
|
45
|
+
def test_public_store_and_from_env_forward_profile(monkeypatch, module_name, class_name):
|
|
46
|
+
import importlib
|
|
47
|
+
module = importlib.import_module('prosaic_harness_postgres.' + module_name)
|
|
48
|
+
captured = []
|
|
49
|
+
monkeypatch.setattr(module, 'PostgresTransport', lambda dsn, **kw: captured.append(kw))
|
|
50
|
+
monkeypatch.setenv('SYNTHETIC_DSN', 'synthetic')
|
|
51
|
+
cls = getattr(module, class_name)
|
|
52
|
+
cls('synthetic', namespace='fixture')
|
|
53
|
+
cls.from_env('SYNTHETIC_DSN', namespace='fixture', durability_profile='aurora-postgresql')
|
|
54
|
+
assert [kw['durability_profile'] for kw in captured] == ['postgresql', 'aurora-postgresql']
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize('command', ['init', 'doctor', 'migrate-conversations'])
|
|
58
|
+
def test_cli_forwards_explicit_profile(monkeypatch, command):
|
|
59
|
+
from prosaic_harness_postgres import cli, session_store
|
|
60
|
+
calls = []
|
|
61
|
+
class Store:
|
|
62
|
+
def __enter__(self): return self
|
|
63
|
+
def __exit__(self, *args): pass
|
|
64
|
+
def initialize(self): pass
|
|
65
|
+
def check_ready(self): pass
|
|
66
|
+
def migrate_context_storage(self): pass
|
|
67
|
+
def create(name, **kwargs):
|
|
68
|
+
calls.append(kwargs['durability_profile'])
|
|
69
|
+
return Store()
|
|
70
|
+
monkeypatch.setattr(cli.PostgresRunStore, 'from_env', create)
|
|
71
|
+
monkeypatch.setattr(session_store.PostgresSessionStore, 'from_env', create)
|
|
72
|
+
assert cli.main([command, '--dsn-env', 'SYNTHETIC_DSN', '--durability-profile', 'aurora-postgresql']) == 0
|
|
73
|
+
assert calls == ['aurora-postgresql']
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize('class_name', ['PostgresRunStore', 'PostgresSessionStore'])
|
|
77
|
+
def test_explicit_profile_retains_real_connection_guards(db_admin_dsn, class_name):
|
|
78
|
+
import prosaic_harness_postgres
|
|
79
|
+
cls = getattr(prosaic_harness_postgres, class_name)
|
|
80
|
+
with cls(db_admin_dsn, namespace='fixture', durability_profile='aurora-postgresql', pool_size=1) as store:
|
|
81
|
+
store.initialize()
|
|
82
|
+
store.check_ready()
|
|
83
|
+
store._transport.call(lambda conn: conn.execute('SET synchronous_commit=off'))
|
|
84
|
+
with pytest.raises(StoreIncompatible):
|
|
85
|
+
store.check_ready()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_public_version_matches_distribution():
|
|
89
|
+
import ast
|
|
90
|
+
from pathlib import Path
|
|
91
|
+
import tomllib
|
|
92
|
+
root = Path(__file__).resolve().parents[1]
|
|
93
|
+
version = tomllib.loads((root/'pyproject.toml').read_text())['project']['version']
|
|
94
|
+
module = ast.parse((root/'src/prosaic_harness_postgres/__init__.py').read_text())
|
|
95
|
+
assert next(ast.literal_eval(n.value) for n in module.body if isinstance(n, ast.Assign)
|
|
96
|
+
and any(isinstance(t, ast.Name) and t.id=='__version__' for t in n.targets)) == version
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_packaging.py
RENAMED
|
@@ -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.
|
|
17
|
-
ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.
|
|
16
|
+
CORE = ROOT / 'dist/b3_prosaic_harness-0.9.4-py3-none-any.whl'
|
|
17
|
+
ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.4.1-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.
|
|
76
|
+
assert core['Version'] == '0.9.4'
|
|
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.
|
|
82
|
-
assert any(value.startswith('b3-prosaic-harness<0.
|
|
81
|
+
assert adapter['Version'] == '0.4.1'
|
|
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'))
|
|
@@ -134,7 +134,8 @@ def test_installed_memory_file_conversations_and_saved_receipt_recovery(wheel_en
|
|
|
134
134
|
report = json.loads(result.stdout)
|
|
135
135
|
assert set(report['stores']) == {'memory', 'file'}
|
|
136
136
|
for evidence in report['stores'].values():
|
|
137
|
-
assert evidence['providerRequests'] ==
|
|
137
|
+
assert evidence['providerRequests'] == 6
|
|
138
|
+
assert evidence['sameMessageFailedRetry'] is True
|
|
138
139
|
assert evidence['savedReceiptAdoptedWithoutDispatch'] is True
|
|
139
140
|
assert evidence['failedCorrectionRetained'] is True
|
|
140
141
|
|
|
@@ -184,7 +185,7 @@ def curated_candidates(release_tool, tmp_path):
|
|
|
184
185
|
candidates = []
|
|
185
186
|
for repo, expected in (
|
|
186
187
|
('B3Cognition/prosaic', {'b3-prosaic': '0.4.1'}),
|
|
187
|
-
('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.
|
|
188
|
+
('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.7',
|
|
188
189
|
'b3-prosaic-runtime-postgres': '0.3.0'})):
|
|
189
190
|
artifacts = []
|
|
190
191
|
for name, version in expected.items():
|
|
@@ -252,4 +253,4 @@ def test_container_mounts_verified_dependency_bytes_read_only(
|
|
|
252
253
|
create = calls[0]
|
|
253
254
|
assert str(wheelhouse) + ':/candidate-wheels:ro' in create
|
|
254
255
|
assert 'PIP_FIND_LINKS=/candidate-wheels' in create[-1]
|
|
255
|
-
assert '/candidate-wheels/b3_prosaic_runtime-0.9.
|
|
256
|
+
assert '/candidate-wheels/b3_prosaic_runtime-0.9.7-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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/tcp_proxy.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/workers.py
RENAMED
|
File without changes
|
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_durability.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_engine.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_examples.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_leases.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_local_cluster.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_native_factory.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_recovery.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_registration.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_routing.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_schema.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_transport.py
RENAMED
|
File without changes
|
{b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_workers.py
RENAMED
|
File without changes
|