|
| 1 | +import fs from "node:fs" |
| 2 | +import os from "node:os" |
| 3 | +import path from "node:path" |
| 4 | +import { spawnSync } from "node:child_process" |
| 5 | + |
| 6 | +const guard = new URL("./codex-automation-connector-rehydration-guard.mjs", import.meta.url).pathname |
| 7 | +const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-automation-connectors-")) |
| 8 | + |
| 9 | +const base = { |
| 10 | + operation_id: "op-auto-connectors-1", |
| 11 | + turn: { |
| 12 | + thread_source: "automation", |
| 13 | + is_followup: true, |
| 14 | + apps_instructions_before: true, |
| 15 | + apps_instructions_after: true, |
| 16 | + explicit_apps_disable: false, |
| 17 | + canonical_thread_preserved: true, |
| 18 | + }, |
| 19 | + connectors: { |
| 20 | + auth_still_valid: true, |
| 21 | + required_families: ["gmail", "calendar"], |
| 22 | + registered_families: ["gmail", "calendar", "drive"], |
| 23 | + read_only_canary_passed: true, |
| 24 | + catalog_revision_readback: true, |
| 25 | + oauth_reconnect_requested: false, |
| 26 | + permission_broadening_requested: false, |
| 27 | + }, |
| 28 | + state: { |
| 29 | + task_state_preserved: true, |
| 30 | + prior_turn_preserved: true, |
| 31 | + external_writes_reconciled: true, |
| 32 | + stale_answer_accepted: false, |
| 33 | + automatic_replay_requested: false, |
| 34 | + unrelated_work_continues: true, |
| 35 | + }, |
| 36 | + continuity_route: { |
| 37 | + type: "approved_openai_connector_runtime", |
| 38 | + verified: true, |
| 39 | + canary_passed: true, |
| 40 | + operation_binding_matches: true, |
| 41 | + }, |
| 42 | +} |
| 43 | + |
| 44 | +function run(name, evidence, expectedCode, expectedReason) { |
| 45 | + const file = path.join(root, `${name}.json`) |
| 46 | + fs.writeFileSync(file, JSON.stringify(evidence)) |
| 47 | + const result = spawnSync(process.execPath, [guard, "--input", file], { encoding: "utf8" }) |
| 48 | + const stream = result.status === 0 ? result.stdout : result.stderr || result.stdout |
| 49 | + const parsed = JSON.parse(stream) |
| 50 | + if (result.status !== expectedCode) throw new Error(`${name}: expected exit ${expectedCode}, got ${result.status}\n${stream}`) |
| 51 | + if (parsed.reason !== expectedReason) throw new Error(`${name}: expected ${expectedReason}, got ${parsed.reason}`) |
| 52 | +} |
| 53 | + |
| 54 | +run("healthy-followup", structuredClone(base), 0, "automation_connector_catalog_verified") |
| 55 | + |
| 56 | +const disabled = structuredClone(base) |
| 57 | +disabled.turn.apps_instructions_after = false |
| 58 | +disabled.connectors.registered_families = [] |
| 59 | +disabled.connectors.read_only_canary_passed = false |
| 60 | +disabled.connectors.catalog_revision_readback = false |
| 61 | +run("silent-disable", disabled, 75, "automation_followup_silently_disabled_apps") |
| 62 | + |
| 63 | +const missingFamily = structuredClone(base) |
| 64 | +missingFamily.connectors.registered_families = ["calendar"] |
| 65 | +run("missing-family", missingFamily, 75, "required_connector_families_missing_after_followup") |
| 66 | + |
| 67 | +const noCanary = structuredClone(base) |
| 68 | +noCanary.connectors.read_only_canary_passed = false |
| 69 | +run("read-canary", noCanary, 75, "connector_read_only_canary_required_after_followup") |
| 70 | + |
| 71 | +const noRevision = structuredClone(base) |
| 72 | +noRevision.connectors.catalog_revision_readback = false |
| 73 | +run("catalog-revision", noRevision, 75, "connector_catalog_revision_not_verified") |
| 74 | + |
| 75 | +const stale = structuredClone(disabled) |
| 76 | +stale.state.stale_answer_accepted = true |
| 77 | +stale.turn.explicit_apps_disable = true |
| 78 | +run("reject-stale-answer", stale, 75, "required_connector_families_missing_after_followup") |
| 79 | + |
| 80 | +const authMutation = structuredClone(disabled) |
| 81 | +authMutation.turn.explicit_apps_disable = true |
| 82 | +authMutation.connectors.required_families = [] |
| 83 | +authMutation.connectors.oauth_reconnect_requested = true |
| 84 | +run("preserve-auth", authMutation, 75, "connector_read_only_canary_required_after_followup") |
| 85 | + |
| 86 | +const replay = structuredClone(base) |
| 87 | +replay.state.automatic_replay_requested = true |
| 88 | +replay.state.external_writes_reconciled = false |
| 89 | +run("reject-replay", replay, 64, "automation_replay_rejected_before_state_and_write_reconciliation") |
| 90 | + |
| 91 | +const globalPause = structuredClone(disabled) |
| 92 | +globalPause.turn.explicit_apps_disable = true |
| 93 | +globalPause.connectors.required_families = [] |
| 94 | +globalPause.connectors.read_only_canary_passed = true |
| 95 | +globalPause.connectors.catalog_revision_readback = true |
| 96 | +globalPause.state.unrelated_work_continues = false |
| 97 | +run("avoid-global-pause", globalPause, 75, "unrelated_automation_work_should_not_be_globally_paused") |
| 98 | + |
| 99 | +const prohibited = structuredClone(base) |
| 100 | +prohibited.continuity_route.type = "model-gateway-auto-select" |
| 101 | +run("prohibited-route", prohibited, 64, "prohibited_route_metadata") |
| 102 | + |
| 103 | +console.log(JSON.stringify({ passed: 10 }, null, 2)) |
0 commit comments