Normalize optional Codex question descriptions before durable validation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 12:24:56 -05:00
1 parent 920fab4050
commit c8fc6047f3
2 files changed
+68 -3

No files matched your search

@@ -3773,11 +3773,17 @@ fn codex_question_set(
}
let option_id = format!("option-{}", index + 1);
labels.insert(option_id.clone(), label.chars().take(240).collect());
Some(json!({
let mut canonical_option = json!({
"id": option_id,
"label": label.chars().take(240).collect::<String>(),
"description": option.get("description").and_then(Value::as_str).map(|value| value.chars().take(1000).collect::<String>()),
}))
});
// Native descriptions are optional/nullable. Canonical input
// permits an omitted description or a string, never null.
if let Some(description) = option.get("description").and_then(Value::as_str) {
canonical_option["description"] =
json!(description.chars().take(1000).collect::<String>());
}
Some(canonical_option)
})
.collect::<Vec<_>>();
if !options.is_empty() && canonical_options.len() != options.len() {
@@ -4593,6 +4599,44 @@ done
);
}
#[test]
fn codex_optional_option_descriptions_produce_schema_valid_resolvable_input() {
let (_, question_set, option_labels) = codex_question_set(
&json!(42),
&json!({"questions":[{
"id":"environment", "question":"Where?",
"options":[
{"label":"Staging"},
{"label":"Production", "description":null},
{"label":"Preview", "description":"Temporary deployment"}
]
}]}),
)
.unwrap();
let options = question_set["questions"][0]["options"].as_array().unwrap();
assert!(options[0].get("description").is_none());
assert!(options[1].get("description").is_none());
assert_eq!(options[2]["description"], "Temporary deployment");
let pending = PendingRuntimeRequest {
rpc_id: json!(42),
turn_id: "turn-1".to_owned(),
method: "item/tool/requestUserInput".to_owned(),
params: Value::Null,
question_set,
option_labels,
retained_bytes: 0,
};
for (index, label) in ["Staging", "Production", "Preview"].iter().enumerate() {
// The canonical response validator also validates the retained
// question-set schema, just as durable presentation does.
let native = codex_question_response(&pending, &json!({
"schema":"paperclip.question_response.v1",
"answers":{"environment":{"selectedOptionIds":[format!("option-{}", index + 1)]}}
})).unwrap();
assert_eq!(native["answers"]["environment"]["answers"], json!([label]));
}
}
#[test]
fn converts_codex_questions_and_responses_without_provider_leakage() {
let (request_id, question_set, labels) = codex_question_set(
@@ -33,6 +33,7 @@ import type {
} from "../protocol/replay-contract.js";
import { executeNativeSession } from "../native-session-runtime.js";
import { NativeSessionCloseUnrecoverableError } from "../contracts/native-session-backend.js";
import { parsePaperclipQuestionSet } from "../contracts/question-set.js";
import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js";
import * as durableControlPlane from "../control-plane/durable-prp-control-plane.js";
@@ -3880,6 +3881,26 @@ it("bridges a runnerd-native question into the server request handler and resolv
},
});
expect(methods).toContain("turn/completed");
// A terminal notification alone does not prove that the question reached
// durable storage or that its answer and provider suffix settled.
await bundle.transport.close();
const runnerState = JSON.parse(await readFile(
join(stateDirectory, "runner", "runner-state.json"), "utf8",
));
expect(runnerState.lifecycle).toBe("suspended");
const controlState = JSON.parse(await readFile(
join(stateDirectory, "control-plane", "control-plane-state.json"), "utf8",
)) as {
committedEvents: Array<{ eventType: string; envelope: { payload: { payload: { request?: { input: unknown } } } } }>;
commands: Array<{ type: string; status: string }>;
};
const created = controlState.committedEvents.filter(event => event.eventType === "runtime_request.created");
expect(created).toHaveLength(1);
const input = parsePaperclipQuestionSet(created[0]!.envelope.payload.payload.request!.input);
expect(input.questions[0]!.options![0]!.description).toBe("Deploy safely.");
expect(input.questions[1]!.options![0]).not.toHaveProperty("description");
expect(controlState.commands.filter(command => command.type === "request.resolve"))
.toEqual([expect.objectContaining({ status: "completed" })]);
} finally {
await bundle.transport.close();
await rm(stateDirectory, { recursive: true, force: true });