사람 승인 게이트를 명시화
This commit is contained in:
parent
707dba4f8f
commit
3a7b6f7760
5 changed files with 133 additions and 3 deletions
|
|
@ -590,6 +590,31 @@ class ContinuousImprovementBoundaryTests(unittest.TestCase):
|
||||||
self.assertIn("executor_receipt_id", schema)
|
self.assertIn("executor_receipt_id", schema)
|
||||||
self.assertIn("executor_evidence_refs", schema)
|
self.assertIn("executor_evidence_refs", schema)
|
||||||
|
|
||||||
|
def test_schema_keeps_reject_and_quarantine_fail_closed_for_every_target(self) -> None:
|
||||||
|
schema_path = (
|
||||||
|
Path(__file__).resolve().parents[3]
|
||||||
|
/ "infra"
|
||||||
|
/ "db"
|
||||||
|
/ "init"
|
||||||
|
/ "14_continuous_improvement.sql"
|
||||||
|
)
|
||||||
|
schema = schema_path.read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
"IF NEW.decision IN ('reject', 'keep_quarantine') THEN",
|
||||||
|
schema,
|
||||||
|
)
|
||||||
|
self.assertIn("IF NEW.decision <> expected_decision", schema)
|
||||||
|
smoke = (
|
||||||
|
Path(__file__).resolve().parents[3]
|
||||||
|
/ "scripts"
|
||||||
|
/ "smoke-continuous-improvement-ledger.sql"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
self.assertIn("'content_qualification'", smoke)
|
||||||
|
self.assertIn("'keep_quarantine'", smoke)
|
||||||
|
self.assertIn("'release_gate'", smoke)
|
||||||
|
self.assertIn("'reject'", smoke)
|
||||||
|
self.assertIn("fail-closed human decision created a side effect", smoke)
|
||||||
|
|
||||||
def test_admin_projection_rejects_raw_transcript_claim(self) -> None:
|
def test_admin_projection_rejects_raw_transcript_claim(self) -> None:
|
||||||
payload = {
|
payload = {
|
||||||
"content_qualifications": [],
|
"content_qualifications": [],
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,12 @@ BEGIN
|
||||||
AND qualified THEN
|
AND qualified THEN
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
IF NEW.decision NOT IN ('reject', expected_decision)
|
-- reject와 keep_quarantine는 부작용 없이 감사 이벤트만 남기는 fail-closed 선택이다.
|
||||||
|
-- 카탈로그·승격·롤백 효과를 만드는 긍정 결정만 계산된 게이트와 일치해야 한다.
|
||||||
|
IF NEW.decision IN ('reject', 'keep_quarantine') THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
IF NEW.decision <> expected_decision
|
||||||
OR (NEW.decision = expected_decision AND NOT qualified) THEN
|
OR (NEW.decision = expected_decision AND NOT qualified) THEN
|
||||||
RAISE EXCEPTION 'human decision cannot bypass computed fail-closed gate' USING ERRCODE = '23514';
|
RAISE EXCEPTION 'human decision cannot bypass computed fail-closed gate' USING ERRCODE = '23514';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
|
||||||
|
|
@ -115,14 +115,17 @@ RELEASE_E2E_TIMEOUT_SECONDS = 15 * 60
|
||||||
|
|
||||||
# Existing preview volumes do not replay docker-entrypoint-initdb.d. These
|
# Existing preview volumes do not replay docker-entrypoint-initdb.d. These
|
||||||
# forward-compatible, idempotent Outcome OS migrations must therefore land
|
# forward-compatible, idempotent Outcome OS migrations must therefore land
|
||||||
# before the new API image is restarted. Every file is applied atomically.
|
# before the new API image is restarted. Transactional files are atomic;
|
||||||
|
# online indexes run outside a transaction so production writes stay available.
|
||||||
RELEASE_DB_MIGRATIONS = (
|
RELEASE_DB_MIGRATIONS = (
|
||||||
"14_continuous_improvement.sql",
|
"14_continuous_improvement.sql",
|
||||||
"15_self_directed_practice_runtime.sql",
|
"15_self_directed_practice_runtime.sql",
|
||||||
"16_calibration_transfer_actual_execution.sql",
|
"16_calibration_transfer_actual_execution.sql",
|
||||||
"17_improvement_workbook_contracts.sql",
|
"17_improvement_workbook_contracts.sql",
|
||||||
|
"18_admin_usage_ledger_index.sql",
|
||||||
"19_auth_identity_alias.sql",
|
"19_auth_identity_alias.sql",
|
||||||
)
|
)
|
||||||
|
RELEASE_DB_ONLINE_MIGRATIONS = frozenset({"18_admin_usage_ledger_index.sql"})
|
||||||
|
|
||||||
# These files are runtime-critical but were added after the first manifest
|
# These files are runtime-critical but were added after the first manifest
|
||||||
# snapshot. A release must classify them as whole-file payloads; otherwise a
|
# snapshot. A release must classify them as whole-file payloads; otherwise a
|
||||||
|
|
@ -133,6 +136,7 @@ REQUIRED_RELEASE_PAYLOAD_PATHS = (
|
||||||
"infra/db/init/15_self_directed_practice_runtime.sql",
|
"infra/db/init/15_self_directed_practice_runtime.sql",
|
||||||
"infra/db/init/16_calibration_transfer_actual_execution.sql",
|
"infra/db/init/16_calibration_transfer_actual_execution.sql",
|
||||||
"infra/db/init/17_improvement_workbook_contracts.sql",
|
"infra/db/init/17_improvement_workbook_contracts.sql",
|
||||||
|
"infra/db/init/18_admin_usage_ledger_index.sql",
|
||||||
"infra/db/init/19_auth_identity_alias.sql",
|
"infra/db/init/19_auth_identity_alias.sql",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -867,6 +871,11 @@ class NasPreviewDeploymentDriver:
|
||||||
migration_commands: list[str] = []
|
migration_commands: list[str] = []
|
||||||
for migration_name in RELEASE_DB_MIGRATIONS:
|
for migration_name in RELEASE_DB_MIGRATIONS:
|
||||||
migration_path = f"{release_root}/infra/db/init/{migration_name}"
|
migration_path = f"{release_root}/infra/db/init/{migration_name}"
|
||||||
|
transaction_flag = (
|
||||||
|
""
|
||||||
|
if migration_name in RELEASE_DB_ONLINE_MIGRATIONS
|
||||||
|
else "--single-transaction "
|
||||||
|
)
|
||||||
migration_commands.extend(
|
migration_commands.extend(
|
||||||
[
|
[
|
||||||
f"test -f {shlex.quote(migration_path)}",
|
f"test -f {shlex.quote(migration_path)}",
|
||||||
|
|
@ -876,7 +885,7 @@ class NasPreviewDeploymentDriver:
|
||||||
f"-f {shlex.quote(compose)} -f {shlex.quote(remote_override)} "
|
f"-f {shlex.quote(compose)} -f {shlex.quote(remote_override)} "
|
||||||
"exec -T db sh -lc "
|
"exec -T db sh -lc "
|
||||||
+ shlex.quote(
|
+ shlex.quote(
|
||||||
'psql -v ON_ERROR_STOP=1 --single-transaction '
|
f"psql -v ON_ERROR_STOP=1 {transaction_flag}"
|
||||||
'-U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
'-U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||||
)
|
)
|
||||||
+ f" < {shlex.quote(migration_path)}"
|
+ f" < {shlex.quote(migration_path)}"
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,51 @@ SELECT set_config('app.current_ai_view','',true);
|
||||||
SELECT set_config('app.current_role','admin',true);
|
SELECT set_config('app.current_role','admin',true);
|
||||||
SELECT set_config('app.current_uid','14000000-0000-0000-0000-000000000001',true);
|
SELECT set_config('app.current_uid','14000000-0000-0000-0000-000000000001',true);
|
||||||
|
|
||||||
|
-- 보류와 반려는 모든 대상에서 감사 기록만 남기고 효과를 만들지 않는다.
|
||||||
|
INSERT INTO app.ci_ingestion_submission (
|
||||||
|
submission_id, content_hash, operation_kind, result_id
|
||||||
|
) VALUES
|
||||||
|
('14000000-0000-0000-0000-000000000064',repeat('8',64),'human_approval','14000000-0000-0000-0000-000000000065'),
|
||||||
|
('14000000-0000-0000-0000-000000000066',repeat('9',64),'human_approval','14000000-0000-0000-0000-000000000067');
|
||||||
|
INSERT INTO audit.ci_human_approval_event (
|
||||||
|
approval_event_id, submission_id, target_kind, target_id, decision, actor_uid,
|
||||||
|
reason_code, evidence_refs, content_hash
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'14000000-0000-0000-0000-000000000065','14000000-0000-0000-0000-000000000064',
|
||||||
|
'content_qualification','14000000-0000-0000-0000-000000000019','keep_quarantine',
|
||||||
|
'14000000-0000-0000-0000-000000000001','more-evidence-required',
|
||||||
|
'{audit://synthetic/g8/content-hold}',repeat('8',64)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'14000000-0000-0000-0000-000000000067','14000000-0000-0000-0000-000000000066',
|
||||||
|
'release_gate','14000000-0000-0000-0000-000000000021','reject',
|
||||||
|
'14000000-0000-0000-0000-000000000001','release-rejected',
|
||||||
|
'{audit://synthetic/g8/release-reject}',repeat('9',64)
|
||||||
|
);
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF (SELECT count(*) FROM audit.ci_human_approval_event
|
||||||
|
WHERE approval_event_id IN (
|
||||||
|
'14000000-0000-0000-0000-000000000065',
|
||||||
|
'14000000-0000-0000-0000-000000000067'
|
||||||
|
)) <> 2
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM app.ci_catalog_entry
|
||||||
|
WHERE approval_event_id = '14000000-0000-0000-0000-000000000065'
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM audit.ci_lifecycle_event
|
||||||
|
WHERE approval_event_id IN (
|
||||||
|
'14000000-0000-0000-0000-000000000065',
|
||||||
|
'14000000-0000-0000-0000-000000000067'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'fail-closed human decision created a side effect';
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
INSERT INTO app.ci_ingestion_submission (
|
INSERT INTO app.ci_ingestion_submission (
|
||||||
submission_id, content_hash, operation_kind, result_id
|
submission_id, content_hash, operation_kind, result_id
|
||||||
) VALUES (
|
) VALUES (
|
||||||
|
|
|
||||||
|
|
@ -388,6 +388,37 @@ class OutcomeReleaseAgentTests(unittest.TestCase):
|
||||||
):
|
):
|
||||||
self.agent_module.validate_required_release_payload(manifest)
|
self.agent_module.validate_required_release_payload(manifest)
|
||||||
|
|
||||||
|
def test_admin_usage_index_migration_is_idempotent_and_release_required(self) -> None:
|
||||||
|
migration_path = (
|
||||||
|
SCRIPT_PATH.parent.parent
|
||||||
|
/ "infra"
|
||||||
|
/ "db"
|
||||||
|
/ "init"
|
||||||
|
/ "18_admin_usage_ledger_index.sql"
|
||||||
|
)
|
||||||
|
migration = migration_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertNotIn("BEGIN;", migration)
|
||||||
|
self.assertIn(
|
||||||
|
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_turns_admin_usage_created_at",
|
||||||
|
migration,
|
||||||
|
)
|
||||||
|
self.assertIn("ON app.turns (created_at DESC)", migration)
|
||||||
|
self.assertIn("WHERE speaker = 'client';", migration)
|
||||||
|
self.assertNotIn("COMMIT;", migration)
|
||||||
|
self.assertIn(
|
||||||
|
"18_admin_usage_ledger_index.sql",
|
||||||
|
self.agent_module.RELEASE_DB_MIGRATIONS,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"18_admin_usage_ledger_index.sql",
|
||||||
|
self.agent_module.RELEASE_DB_ONLINE_MIGRATIONS,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"infra/db/init/18_admin_usage_ledger_index.sql",
|
||||||
|
self.agent_module.REQUIRED_RELEASE_PAYLOAD_PATHS,
|
||||||
|
)
|
||||||
|
|
||||||
def test_two_patch_runs_must_be_byte_identical_before_any_deploy(self) -> None:
|
def test_two_patch_runs_must_be_byte_identical_before_any_deploy(self) -> None:
|
||||||
agent, runner, deployment, _, _ = self.make_agent(
|
agent, runner, deployment, _, _ = self.make_agent(
|
||||||
active_sha=None,
|
active_sha=None,
|
||||||
|
|
@ -740,6 +771,7 @@ class OutcomeReleaseAgentTests(unittest.TestCase):
|
||||||
"15_self_directed_practice_runtime.sql",
|
"15_self_directed_practice_runtime.sql",
|
||||||
"16_calibration_transfer_actual_execution.sql",
|
"16_calibration_transfer_actual_execution.sql",
|
||||||
"17_improvement_workbook_contracts.sql",
|
"17_improvement_workbook_contracts.sql",
|
||||||
|
"18_admin_usage_ledger_index.sql",
|
||||||
"19_auth_identity_alias.sql",
|
"19_auth_identity_alias.sql",
|
||||||
),
|
),
|
||||||
self.agent_module.RELEASE_DB_MIGRATIONS,
|
self.agent_module.RELEASE_DB_MIGRATIONS,
|
||||||
|
|
@ -924,6 +956,20 @@ class OutcomeReleaseAgentTests(unittest.TestCase):
|
||||||
self.assertIn("required env file missing", prepare_command)
|
self.assertIn("required env file missing", prepare_command)
|
||||||
self.assertIn(f"--env-file {root}/release/infra/.env", promote_command)
|
self.assertIn(f"--env-file {root}/release/infra/.env", promote_command)
|
||||||
self.assertNotIn(f"--env-file {root}/infra/.env", promote_command)
|
self.assertNotIn(f"--env-file {root}/infra/.env", promote_command)
|
||||||
|
transactional_migration_command = next(
|
||||||
|
line
|
||||||
|
for line in promote_command.splitlines()
|
||||||
|
if "17_improvement_workbook_contracts.sql" in line
|
||||||
|
and "psql -v ON_ERROR_STOP=1" in line
|
||||||
|
)
|
||||||
|
online_migration_command = next(
|
||||||
|
line
|
||||||
|
for line in promote_command.splitlines()
|
||||||
|
if "18_admin_usage_ledger_index.sql" in line
|
||||||
|
and "psql -v ON_ERROR_STOP=1" in line
|
||||||
|
)
|
||||||
|
self.assertIn("--single-transaction", transactional_migration_command)
|
||||||
|
self.assertNotIn("--single-transaction", online_migration_command)
|
||||||
self.assertEqual(f"{root}/release/infra/.env", state["env_file"])
|
self.assertEqual(f"{root}/release/infra/.env", state["env_file"])
|
||||||
|
|
||||||
def test_adopted_legacy_state_infers_compose_adjacent_env_file(self) -> None:
|
def test_adopted_legacy_state_infers_compose_adjacent_env_file(self) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue