Codex use case
Transformez un ticket en pull request testée
Confiez à Codex le développement d’une petite fonctionnalité avec ses critères d’acceptation, puis vérifiez les résultats pour chaque critère.
Donnez à Codex un ticket au périmètre défini, un environnement cloud déjà préparé et une liste précise de vérifications. Vous obtenez une modification prête pour la revue de code, avec les tests, la documentation et les éléments de validation pour chaque critère d’acceptation.
Best for
- Ingénieurs de startups chargés de modifier une API à partir d’un ticket au périmètre défini.
- Responsables de la revue de code qui ont besoin d’examiner ensemble la modification et les résultats des vérifications.
- Équipes disposant de critères d’acceptation et d’un environnement de test opérationnel.
Contents
Transformez un ticket en pull request testée
Confiez à Codex le développement d’une petite fonctionnalité avec ses critères d’acceptation, puis vérifiez les résultats pour chaque critère.
Donnez à Codex un ticket au périmètre défini, un environnement cloud déjà préparé et une liste précise de vérifications. Vous obtenez une modification prête pour la revue de code, avec les tests, la documentation et les éléments de validation pour chaque critère d’acceptation.
Donnez à Codex un ticket au périmètre défini, un environnement cloud déjà préparé et une liste précise de vérifications. Vous obtenez une modification prête pour la revue de code, avec les tests, la documentation et les éléments de validation pour chaque critère d’acceptation.
Best for
- Ingénieurs de startups chargés de modifier une API à partir d’un ticket au périmètre défini.
- Responsables de la revue de code qui ont besoin d’examiner ensemble la modification et les résultats des vérifications.
- Équipes disposant de critères d’acceptation et d’un environnement de test opérationnel.
Starter prompt
Confiez à Codex un ticket de portée limitée et bien défini. Demandez-lui de fournir à votre collègue tout le nécessaire pour la revue de code : l’implémentation, les tests, la documentation et les résultats qui démontrent que le comportement demandé fonctionne. Réalisez toute la tâche dans un même environnement cloud préparé à l’avance pour que le développement et la validation utilisent la même configuration du dépôt.
-
Dans l’application web ou dans l’application de bureau ChatGPT, choisissez Travailler dans > Cloud, puis sélectionnez un environnement publié.
-
Pour en créer un, sélectionnez Créer un environnement, choisissez les dépôts GitHub et demandez à Codex de préparer l’environnement d’exécution, les dépendances et les services de test isolés.
-
Consultez le rapport de configuration, corrigez les problèmes signalés par les vérifications, puis sélectionnez Publier avant de lancer une nouvelle tâche.
- Suivez le guide à jour des environnements cloud.
- Consignez les instructions propres au dépôt dans AGENTS.md.
Si les dépendances ou la configuration des services changent, modifiez l’environnement réutilisable, testez-le et sélectionnez Republier. Vérifiez ensuite les changements dans une nouvelle tâche.
L’actualisation du dépôt ne relance pas les commandes d’installation ou de démarrage. Les tâches existantes conservent leur propre état.
Configurez séparément les destinations réseau requises et les identifiants de test. Autoriser une destination ne donne pas accès au service.
Si vous ne pouvez pas exécuter les vérifications requises, terminez la configuration avant d’utiliser ce workflow.
Quand utiliser ce workflow
Choisissez une fonctionnalité dont le contrat est écrit et pour laquelle vous savez précisément quand le travail sera terminé : un nouveau point de terminaison d’API, une règle de validation ou un ajout limité à un service existant. Le ticket doit pouvoir être traité en une seule pull request, avec des vérifications exécutables sans accès à la production.
L’exemple ajoute la pagination au flux d’activité d’un projet. L’exemple exécutable en Node.js comprend l’authentification, le contrôle d’accès aux projets, des curseurs signés et une route existante qui renvoie les détails d’un projet. Les données de test sont fixes et conservées en mémoire. Vous pouvez ainsi vérifier le contrat de pagination sans base de données ni service externe.
Essayez l’exemple exécutable
Téléchargez l’exemple, extrayez l’archive et ouvrez le dossier issue-to-tested-pr. Utilisez Node 22.6 ou une version ultérieure. L’exemple utilise la bibliothèque standard et ne nécessite ni installation de paquets ni clé d’API.
node --version
node api.mjs
node test.mjs --seeded
node test.mjs
node reproduce.mjs ./evidence
node api.mjs démarre un serveur HTTP local, demande la première page, affiche les identifiants publics des événements qu’elle contient, puis ferme le serveur.
Avec --seeded, les tests se terminent volontairement avec un code de sortie non nul. La pagination reprend à partir du seul horodatage et saute deux événements qui ont le même horodatage que le dernier événement de la première page. Les mêmes tests réussissent une fois la reprise corrigée pour utiliser le couple horodatage-identifiant.
La commande de reproduction ne réussit que si les tests échouent avec le défaut introduit et réussissent après correction.
Avec la correction et limit=2, le parcours renvoie ces pages en trois requêtes :
[evt_105, evt_104]
[evt_103, evt_102]
[evt_101]
Les deux premières pages contiennent un curseur de continuation. La dernière renvoie next_cursor: null.
La version défectueuse renvoie [evt_105, evt_104], puis [evt_101], en omettant evt_103 et evt_102.
Lors d’une exécution réussie, la version corrigée passe les 36 tests. Avec la version défectueuse, trois assertions échouent et mettent en évidence le défaut de pagination.
Pour vérifier votre environnement, placez les fichiers dans le dépôt choisi, préparez et publiez un environnement avec une version de Node prise en charge, puis exécutez les commandes dans une nouvelle tâche dans le cloud et consignez les résultats.
reproduce.mjs enregistre red.log, green.log, observed-http.json et run-manifest.json dans le répertoire de résultats demandé. Le manifeste indique l’environnement d’exécution, les commandes, les codes de sortie, le défaut introduit, sa correction et les empreintes SHA-256 des fichiers sources.
Il n’enregistre ni les tokens de session ni la clé de signature. Consultez README.md et openapi.json pour connaître la configuration de l’exemple et son contrat public.
Examinez le code de l’exemple
Ces extraits proviennent des fichiers de l’archive. Après avoir extrait le ZIP, exécutez les fichiers complets.
Implémentation du curseur et du tri
export function createApi({
sessions,
projects,
events,
signingKey,
seededBug = false,
}) {
return createServer((request, response) => {
const send = (status, body) => {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
};
const fail = (code) => {
const [status, message] = errors[code];
send(status, { error: { code, message } });
};
// Authenticate, then authorize. Malformed parameters must not change these results.
const authorization = request.headers.authorization ?? "";
const bearer = /^Bearer ([A-Za-z0-9_-]+)$/.exec(authorization);
const tenantId = bearer ? sessions.get(bearer[1]) : undefined;
if (!tenantId) return fail("unauthenticated");
const url = new URL(request.url ?? "/", "http://127.0.0.1");
const route = /^\/v1\/projects\/([^/]+)(\/events)?$/.exec(url.pathname);
let projectId;
try {
projectId = route ? decodeURIComponent(route[1]) : undefined;
} catch {
return fail("not_found");
}
const project = projectId ? projects.get(projectId) : undefined;
if (
request.method !== "GET" ||
!project ||
project.tenant_id !== tenantId
) {
return fail("not_found");
}
// Existing project-detail behavior remains independent of pagination parameters.
if (!route[2]) return send(200, { id: projectId, name: project.name });
const rawLimit = url.searchParams.get("limit");
const limit = rawLimit === null ? 25 : Number(rawLimit);
if (
url.searchParams.getAll("limit").length > 1 ||
(rawLimit !== null && !/^\d+$/.test(rawLimit)) ||
!Number.isInteger(limit) ||
limit < 1 ||
limit > 100
) {
return fail("invalid_limit");
}
let cursor;
if (url.searchParams.has("cursor")) {
try {
if (url.searchParams.getAll("cursor").length !== 1)
throw new Error("Invalid cursor");
cursor = decodeCursor(
url.searchParams.get("cursor"),
tenantId,
projectId,
signingKey
);
} catch {
return fail("invalid_cursor");
}
}
const matching = events
.filter(
(event) =>
event.tenant_id === tenantId && event.project_id === projectId
)
.sort(compareEvents)
.filter((event) => {
if (!cursor) return true;
// Deliberate baseline defect: timestamp-only continuation loses tied rows.
if (seededBug)
return (
timestampValue(event.created_at) < timestampValue(cursor.created_at)
);
return compareEvents(event, cursor) > 0;
});
const selected = matching.slice(0, limit);
const items = selected.map(({ id, created_at, type, summary }) => ({
id,
created_at,
type,
summary,
}));
const nextCursor =
matching.length > limit
? encodeCursor(selected.at(-1), tenantId, projectId, signingKey)
: null;
return send(200, { items, next_cursor: nextCursor });
});
}
export async function startApi(fixtures, options = {}) {
const server = createApi({ ...fixtures, ...options });
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => resolve(undefined));
});
const address = server.address();
if (!address || typeof address === "string")
throw new Error("No listening address");
return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: () =>
new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve(undefined)))
),
};
}Données initiales et jeux de test pour l’accès aux projets
export function createFixtures() {
const tokenA = randomBytes(24).toString("base64url");
const tokenB = randomBytes(24).toString("base64url");
const sessions = new Map([
[tokenA, "tenant_a"],
[tokenB, "tenant_b"],
]);
const projects = new Map([
["feed", { tenant_id: "tenant_a", name: "Activity feed" }],
["empty", { tenant_id: "tenant_a", name: "Empty feed" }],
["exact", { tenant_id: "tenant_a", name: "Four events" }],
["large", { tenant_id: "tenant_a", name: "Large feed" }],
["precision", { tenant_id: "tenant_a", name: "Precise timestamps" }],
["other", { tenant_id: "tenant_a", name: "Another project" }],
["private", { tenant_id: "tenant_b", name: "Private project" }],
]);
const event = (id, created_at, project_id, tenant_id = "tenant_a") => ({
id,
created_at,
project_id,
tenant_id,
type: "deployment",
summary: `Synthetic event ${id}`,
internal_note: "This field must never enter the HTTP response.",
raw_payload: { synthetic: true },
});
const feed = [
event("evt_105", "2026-01-15T12:00:03.123456Z", "feed"),
event("evt_104", "2026-01-15T12:00:02.123456Z", "feed"),
event("evt_103", "2026-01-15T12:00:02.123456Z", "feed"),
event("evt_102", "2026-01-15T12:00:02.123456Z", "feed"),
event("evt_101", "2026-01-15T12:00:01.123456Z", "feed"),
];
const events = [
...feed,
...feed.slice(0, 4).map((item) => ({ ...item, project_id: "exact" })),
...Array.from({ length: 105 }, (_, index) =>
event(
`large_${String(index).padStart(3, "0")}`,
new Date(Date.UTC(2026, 0, 15, 12, 0, index))
.toISOString()
.replace(".000Z", ".123456Z"),
"large"
)
),
// Reverse ID order makes timestamp precision observable in the tests.
event("precise_a", "2026-01-15T12:00:00.123999Z", "precision"),
event("precise_z", "2026-01-15T12:00:00.123001Z", "precision"),
event("other_1", "2026-01-15T12:00:00.123456Z", "other"),
event("other_2", "2026-01-15T11:59:59.123456Z", "other"),
event("private_1", "2026-01-15T12:00:00.123456Z", "private", "tenant_b"),
event("private_2", "2026-01-15T11:59:59.123456Z", "private", "tenant_b"),
];
return {
sessions,
projects,
events,
signingKey: randomBytes(32),
tokenA,
tokenB,
};
}Contrat HTTP et tests de non-régression
test("five-event traversal crosses a timestamp tie and replays the same continuation", async () => {
const expected = ["evt_105", "evt_104", "evt_103", "evt_102", "evt_101"];
const pages = await traverse("feed", 2, expected);
assert.deepEqual(pages.map(ids), [
["evt_105", "evt_104"],
["evt_103", "evt_102"],
["evt_101"],
]);
assert.equal(pages.length, 3);
assert.ok(pages[0].next_cursor);
assert.ok(pages[1].next_cursor);
assert.equal(pages[2].next_cursor, null);
const replay = await request("feed", {
limit: "2",
cursor: pages[0].next_cursor,
});
assert.deepEqual(replay.body.items, pages[1].items);
const next = await request("feed", {
limit: "2",
cursor: replay.body.next_cursor,
});
assert.deepEqual(next.body.items, pages[2].items);
assert.equal(next.body.next_cursor, null);
console.log(
JSON.stringify({
fixture: "five-event-tie",
pages: pages.map(ids),
requests: pages.length,
replay: ids(replay.body),
})
);
});
test("the same tie group traverses correctly with limit three", async () => {
const pages = await traverse("feed", 3, [
"evt_105",
"evt_104",
"evt_103",
"evt_102",
"evt_101",
]);
assert.deepEqual(pages.map(ids), [
["evt_105", "evt_104", "evt_103"],
["evt_102", "evt_101"],
]);
});
test("timestamp ordering and continuation preserve submillisecond precision", async () => {
const pages = await traverse("precision", 1, ["precise_a", "precise_z"]);
assert.equal(pages[0].items[0].created_at, "2026-01-15T12:00:00.123999Z");
assert.equal(pages[1].items[0].created_at, "2026-01-15T12:00:00.123001Z");
});
test("omitted, minimum, and maximum limits return 25, 1, and 100 of 105 events", async () => {
for (const [parameters, expected] of [
[{}, 25],
[{ limit: "1" }, 1],
[{ limit: "100" }, 100],
]) {
const result = await request("large", parameters);
assert.equal(result.status, 200);
assert.equal(result.body.items.length, expected);
assert.ok(result.body.next_cursor);
}
const pages = await traverse(
"large",
100,
Array.from(
{ length: 105 },
(_, index) => `large_${String(104 - index).padStart(3, "0")}`
)
);
assert.deepEqual(
pages.map((page) => page.items.length),
[100, 5]
);
});
test("invalid and repeated limits return the documented generic error", async (context) => {
for (const limit of [
"0",
"101",
"-1",
"1.5",
"many",
"",
"1e2",
"Infinity",
]) {
await context.test(`limit ${JSON.stringify(limit)}`, async () => {
const result = await request("feed", { limit });
assert.equal(result.status, 400);
assert.deepEqual(
result.body,
expectedError(
"invalid_limit",
"Limit must be an integer from 1 through 100."
)
);
});
}
const repeated = await request("feed", [
["limit", "1"],
["limit", "2"],
]);
assert.equal(repeated.status, 400);
});
test("empty, partial, and exactly full final pages have no continuation", async () => {
assert.deepEqual((await request("empty")).body, {
items: [],
next_cursor: null,
});
const partial = await traverse("feed", 2, [
"evt_105",
"evt_104",
"evt_103",
"evt_102",
"evt_101",
]);
assert.equal(partial.at(-1).items.length, 1);
const exact = await traverse("exact", 2, [
"evt_105",
"evt_104",
"evt_103",
"evt_102",
]);
assert.deepEqual(
exact.map((page) => page.items.length),
[2, 2]
);
assert.equal(exact[1].next_cursor, null);
});
test("authentication precedes malformed and wrong-scope cursor or limit validation", async () => {
const privateCursor = (
await request("private", { limit: "1" }, fixtures.tokenB)
).body.next_cursor;
for (const token of [null, "invalid_session"]) {
for (const parameters of [
{ cursor: "broken" },
{ cursor: privateCursor },
{ limit: "0", cursor: "broken" },
]) {
const result = await request("feed", parameters, token);
assert.equal(result.status, 401);
assert.deepEqual(result.body, unauthenticated);
}
}
});
test("inaccessible and nonexistent projects return identical 404 before cursor validation", async () => {
const ownCursor = (await request("feed", { limit: "1" })).body.next_cursor;
for (const project of ["private", "missing"]) {
for (const parameters of [
{},
{ cursor: "broken" },
{ cursor: ownCursor },
{ limit: "0", cursor: "broken" },
]) {
const result = await request(project, parameters);
assert.equal(result.status, 404);
assert.deepEqual(result.body, notFound);
}
}
});
test("a valid cursor from another project in the same tenant is invalid on an authorized feed", async () => {
const cursor = (await request("other", { limit: "1" })).body.next_cursor;
assert.ok(cursor);
const result = await request("feed", { cursor });
assert.equal(result.status, 400);
assert.deepEqual(result.body, invalidCursor);
});
test("a valid cursor from another tenant is invalid on an authorized feed", async () => {
const cursor = (await request("private", { limit: "1" }, fixtures.tokenB))
.body.next_cursor;
assert.ok(cursor);
const result = await request("feed", { cursor });
assert.equal(result.status, 400);
assert.deepEqual(result.body, invalidCursor);
});
test("undecodable, empty, tampered, and repeated cursor tokens return the same closed error", async () => {
const cursor = (await request("feed", { limit: "1" })).body.next_cursor;
const [payload, signature] = cursor.split(".");
const tampered = `${payload}.${signature[0] === "A" ? "B" : "A"}${signature.slice(1)}`;
for (const token of [
"broken",
"",
"%%%.___",
"a.b.c",
"x".repeat(2049),
tampered,
]) {
const result = await request("feed", { cursor: token });
assert.equal(result.status, 400);
assert.deepEqual(result.body, invalidCursor);
}
const result = await request("feed", [
["cursor", cursor],
["cursor", cursor],
]);
assert.equal(result.status, 400);
assert.deepEqual(result.body, invalidCursor);
});
test("signed structurally invalid payloads and unsupported versions are rejected", async (context) => {
const valid = {
version: 1,
tenant_id: "tenant_a",
project_id: "feed",
created_at: "2026-01-15T12:00:02.123456Z",
id: "evt_104",
};
const missing = { ...valid };
delete missing.id;
const cases = [
["missing ID", missing],
["number ID", { ...valid, id: 104 }],
["empty ID", { ...valid, id: "" }],
["number timestamp", { ...valid, created_at: 0 }],
["invalid date", { ...valid, created_at: "2026-02-30T12:00:00Z" }],
["unsupported version", { ...valid, version: 2 }],
["extra field", { ...valid, extra: true }],
["wrong tenant type", { ...valid, tenant_id: 1 }],
["wrong project type", { ...valid, project_id: null }],
["null payload", null],
["array payload", []],
["invalid JSON", "not-json"],
];
for (const [name, payload] of cases) {
await context.test(name, async () => {
const result = await request("feed", { cursor: signFixture(payload) });
assert.equal(result.status, 400);
assert.deepEqual(result.body, invalidCursor);
});
}
});
test("every serialized event omits stored tenant, project, and raw payload fields", async () => {
const result = await request("feed", { limit: "100" });
assert.equal(result.status, 200);
for (const item of result.body.items) {
assert.deepEqual(Object.keys(item).sort(), [
"created_at",
"id",
"summary",
"type",
]);
}
assert.ok(
fixtures.events.some((event) => event.internal_note && event.raw_payload)
);
});
test("the schema oracle rejects extra, missing, incorrectly typed, and malformed public fields", () => {
const schema = specification.components.schemas.EventPage;
const page = {
items: [
{
id: "evt_1",
created_at: "2026-01-15T12:00:00.123456Z",
type: "deployment",
summary: "Synthetic",
},
],
next_cursor: null,
};
assert.ok(conforms(page, schema));
assert.equal(conforms({ ...page, tenant_id: "tenant_a" }, schema), false);
assert.equal(
conforms(
{ ...page, items: [{ ...page.items[0], raw_payload: {} }] },
schema
),
false
);
assert.equal(conforms({ items: page.items }, schema), false);
assert.equal(conforms({ ...page, next_cursor: "" }, schema), false);
assert.equal(
conforms({ ...page, items: [{ ...page.items[0], id: 1 }] }, schema),
false
);
assert.equal(
conforms(
{ ...page, items: [{ ...page.items[0], created_at: "yesterday" }] },
schema
),
false
);
const errorSchema = specification.components.schemas.InvalidCursor;
assert.equal(
conforms(
{ error: { ...invalidCursor.error, stack: "internal" } },
errorSchema
),
false
);
assert.equal(
conforms(
{ error: { ...invalidCursor.error, message: "tenant_a" } },
errorSchema
),
false
);
});
test("the existing project-detail route preserves access and ignores pagination parameters", async () => {
const own = await request(
"feed",
{ cursor: "broken", limit: "0" },
fixtures.tokenA,
true
);
assert.equal(own.status, 200);
assert.deepEqual(own.body, { id: "feed", name: "Activity feed" });
const inaccessible = await request("private", {}, fixtures.tokenA, true);
assert.equal(inaccessible.status, 404);
assert.deepEqual(inaccessible.body, notFound);
const unauthenticatedResult = await request("feed", {}, null, true);
assert.equal(unauthenticatedResult.status, 401);
assert.deepEqual(unauthenticatedResult.body, unauthenticated);
});
test("OpenAPI declares operation, limit boundaries, signed cursor, and response statuses", () => {
assert.equal(specification.openapi, "3.1.0");
const operation = specification.paths["/v1/projects/{project_id}/events"].get;
assert.equal(operation.operationId, "listProjectEvents");
const limit = operation.parameters.find(
(parameter) => parameter.name === "limit"
).schema;
assert.deepEqual(limit, {
type: "integer",
minimum: 1,
maximum: 100,
default: 25,
});
assert.deepEqual(Object.keys(operation.responses).sort(), [
"200",
"400",
"401",
"404",
]);
assert.equal(
specification.components.securitySchemes.session.scheme,
"bearer"
);
});Enregistrement des résultats de vérification
mkdirSync(output, { recursive: true });
const states = [];
for (const [name, arguments_] of [
["red", ["--seeded"]],
["green", []],
]) {
const result = spawnSync(
process.execPath,
[resolve(directory, "test.mjs"), ...arguments_],
{
cwd: directory,
encoding: "utf8",
timeout: 30_000,
}
);
writeFileSync(
resolve(output, `${name}.log`),
`${result.stdout ?? ""}${result.stderr ?? ""}`
);
states.push({
name,
command: `node test.mjs${arguments_.length ? " --seeded" : ""}`,
exit_code: result.status,
signal: result.signal,
error: result.error?.message ?? null,
});
}
const sources = Object.fromEntries(
readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => [
entry.name,
createHash("sha256")
.update(readFileSync(resolve(directory, entry.name)))
.digest("hex"),
])
);
const observed = { red: await observe(true), green: await observe(false) };
writeJson("observed-http.json", observed);
writeJson("run-manifest.json", {
execution: "Synthetic loopback HTTP fixture; no external service",
recorded_at: new Date().toISOString(),
node: process.version,
minimum_node: "22.6.0",
dependencies: "Node standard library only; no install step",
baseline:
"Deliberately seeded timestamp-only continuation; not a historical customer defect",
correction:
"Descending timestamp-and-ID tuple continuation using full fractional precision",
source_sha256: sources,
states,
});
if (
states[0].exit_code === null ||
states[0].exit_code === 0 ||
states[1].exit_code !== 0
) {
throw new Error(
"Expected the unchanged tests to fail on the seeded baseline and pass on the corrected implementation. Inspect red.log and green.log."
);
}
console.log(
JSON.stringify(
{
execution: "synthetic-loopback-http",
red_exit: states[0].exit_code,
green_exit: states[1].exit_code,
green_pages: observed.green.pages.map((page) =>
page.body.items.map((item) => item.id)
),
evidence_directory: output,
},
null,
2
)
);Préparez les informations à fournir
Un lien vers un ticket n’est utile que si Codex peut le consulter. Si la source est inaccessible, collez le contenu pertinent ou joignez-le à la tâche. Fournissez les informations suivantes avec le prompt initial.
| Information | Exemple de ticket |
|---|---|
| Ticket et résultat attendu | API-142 : ajoutez GET /v1/projects/{project_id}/events pour permettre aux clients de parcourir un flux d’activité page par page. |
| Dépôt et révision de base | Placez le dossier de l’exemple dans le dépôt choisi, puis consignez la branche et le commit exact sélectionnés pour la tâche. |
| Code pertinent | Les fichiers api.mjs, fixtures.mjs, test.mjs et openapi.json de l’exemple, ainsi que le fichier AGENTS.md de votre dépôt. |
| Conventions existantes | Authentifiez la session et contrôlez l’accès au projet avant de valider les paramètres de pagination. Utilisez des curseurs signés et la structure publique documentée pour les erreurs. Si vous adaptez le ticket, utilisez les fonctions utilitaires existantes du dépôt. |
| Configuration des tests | Projets de test fixes en mémoire pour deux tenants, six projets pour le tenant A, événements de même horodatage répartis sur deux pages, horodatages avec fractions de seconde, dernières pages vides ou exactement pleines et 105 événements pour vérifier les limites. |
| Validation | node test.mjs --seeded (échec attendu), node test.mjs (version corrigée) et node reproduce.mjs ./evidence. Exécutez node --check pour chaque fichier .mjs. Pour votre service, ajoutez les commandes effectivement utilisées pour la vérification des types, le lint, les tests de non-régression et la CI. |
| Périmètre | Le point de terminaison, ses tests ciblés et sa documentation OpenAPI. Aucune nouvelle dépendance, migration de base de données, modification du frontend, opération de déploiement ou refonte de l’authentification et des autorisations. |
| Livrable | Une PR en brouillon si l’intégration au dépôt le permet et que vous en avez autorisé la création. Sinon, un diff et une proposition de description pour la PR. |
Pour l’exemple, prévoyez Node 22.6 ou une version ultérieure. Les tests créent leurs propres sessions et leur propre clé de signature des curseurs, puis démarrent un serveur HTTP temporaire sur l’interface de bouclage. Pour votre service, préparez aussi les dépendances, la configuration des tests et les services isolés. Avant de confier le ticket à Codex, vérifiez que la fonction de gestion des curseurs peut associer un curseur au tenant authentifié et au projet demandé.
Définissez le contrat de l’API
Avant de commencer l’implémentation, vérifiez que le contrat de l’exemple décrit ci-dessous correspond à celui de votre dépôt. En cas de succès, l’API renvoie un statut HTTP 200 et un corps contenant exactement { items, next_cursor }.
Chaque élément comporte exactement quatre champs publics obligatoires : id (chaîne non vide), created_at (chaîne représentant un horodatage UTC au format RFC 3339), type (chaîne non vide) et summary (chaîne). items est un tableau de ces objets. next_cursor est soit une chaîne opaque non vide, soit null.
Les clés internes des tenants et des projets, les métadonnées de la base de données, les données brutes des événements et les identifiants d’accès ne figurent pas dans la réponse. Dans OpenAPI, décrivez le type des champs de réponse, les champs obligatoires, ceux qui acceptent null et la structure des objets qui n’admettent aucun champ supplémentaire. Vérifiez que les corps de réponse HTTP sérialisés respectent ce schéma.
Les événements sont triés selon created_at DESC, id DESC, en conservant la précision des fractions de seconde et en triant les identifiants par ordre lexicographique décroissant. Le curseur reprend strictement après le dernier couple horodatage-identifiant renvoyé dans cet ordre.
La taille de page par défaut est de 25. Les limites valides sont des entiers décimaux compris entre 1 et 100 inclus. Une requête qui répète le paramètre de limite ou de curseur est invalide. Renvoyez un curseur non nul uniquement s’il reste au moins un événement correspondant à la requête.
Une dernière page partiellement remplie ou exactement pleine renvoie next_cursor: null. Un projet vide auquel l’accès est autorisé renvoie items: [] et next_cursor: null.
L’exemple signe chaque curseur et l’associe au tenant authentifié et au projet demandé. Authentifiez la session et vérifiez l’accès au projet demandé avant de valider la limite ou le curseur. Ainsi, une erreur de paramètre ne peut pas révéler l’existence d’un projet appartenant à un autre tenant.
Une session absente ou invalide entraîne une réponse 401. Un projet inaccessible et un projet inexistant donnent lieu à la même réponse 404 générique. Une fois l’accès confirmé, vérifiez la limite avant le curseur.
Si le curseur est mal formé, altéré, de structure invalide ou associé à un autre tenant ou projet, l’API renvoie un statut HTTP 400 avec le code invalid_cursor. Même valide pour un autre projet du même tenant ou pour un autre tenant, un curseur est invalide si vous le fournissez pour le projet auquel vous avez accès.
La présence d’un curseur ne doit pas modifier le résultat du contrôle d’accès à un projet inaccessible. Dans l’exemple, les curseurs de version 1 n’expirent pas. Les versions non prises en charge sont invalides.
Dans l’exemple, chaque corps de réponse d’erreur contient exactement { error: { code, message } }. Les deux champs sont obligatoires, de type chaîne, et aucun champ supplémentaire n’est autorisé.
Utilisez les réponses d’erreur génériques suivantes :
- 400
invalid_cursor/Invalid cursor. - 400
invalid_limit/Limit must be an integer from 1 through 100. - 401
unauthenticated/Authentication required. - 404
not_found/Project not found.
Excluez de tous les corps de réponse d’erreur le contenu décodé des curseurs, les identifiants de tenant ou de projet, les erreurs brutes et les traces de pile. Si votre dépôt prévoit une autre structure ou d’autres codes, utilisez ceux qui y sont approuvés. Réglez cette divergence de contrat avant de modifier des fonctions utilitaires partagées.
Les assertions sur le parcours et la réutilisation d’un curseur s’appuient sur des jeux de données fixes, sans écritures concurrentes. En suivant les curseurs, vous devez obtenir chaque événement une seule fois, dans l’ordre convenu, en au plus ceil(event_count / limit) requêtes pour un flux non vide, ou en une seule requête pour un flux vide.
Tant que les données de test restent inchangées, réutiliser un curseur valide avec les mêmes paramètres de requête doit renvoyer les mêmes éléments et la même position de reprise. Les tokens signés peuvent différer au niveau des octets.
Ce contrat ne garantit ni des curseurs à usage unique, ni une pagination sur un instantané, ni des résultats identiques après modification du flux. Documentez les garanties de cohérence du flux déjà définies dans le dépôt. Si aucune règle n’existe et que le ticket exige une pagination sur un instantané, suspendez le travail le temps d’obtenir une décision de l’équipe produit.
Utilisez le workflow
-
Choisissez le ticket et préparez l’environnement. Vérifiez que le contrat ci-dessus correspond à celui du dépôt. Sélectionnez l’environnement cloud préparé et le commit de base prévu. Fournissez les informations du ticket directement dans la tâche. Vous pouvez lancer ce workflow sans déclencheur automatique dans l’outil de suivi des tickets.
-
Lancez le prompt initial. Remplacez les champs entre crochets par le dépôt, les informations du ticket, le périmètre et les vérifications à effectuer. Précisez si Codex doit renvoyer un diff ou créer une PR en brouillon avec votre autorisation. Le prompt initial de cette page contient toutes les instructions relatives aux livrables.
-
Vérifiez l’état initial. Codex doit lire les instructions du dépôt et exécuter les vérifications initiales approuvées avant toute modification.
Pour l’exemple, exécutez
node test.mjset les vérifications de syntaxe indiquées dansREADME.md. L’option--seededsert à une démonstration distincte où l’échec est volontaire.Pour votre service, exécutez les tests de non-régression, la vérification des types et le lint habituels. Si le fichier de tests ciblés du nouveau point de terminaison n’existe pas encore, indiquez que ces tests restent à créer. Corrigez les échecs des vérifications existantes et rétablissez les services de test indisponibles avant d’attribuer les résultats ultérieurs à la fonctionnalité.
-
Implémentez et vérifiez le contrat. Demandez à Codex d’utiliser les fonctions existantes de contrôle d’accès et de gestion des curseurs, d’implémenter le point de terminaison, d’ajouter les tests d’acceptation ci-dessous et de mettre à jour sa documentation d’API. Dans la mesure du possible, les nouveaux tests doivent échouer sur le code d’origine et réussir après modification. Joignez ces résultats au compte rendu final des vérifications. Dans l’exemple téléchargeable,
--seededactive un défaut volontaire pour permettre cette comparaison. Distinguez cette démonstration d’un échec des vérifications initiales dans votre dépôt. -
Examinez la modification et les résultats de vérification. Examinez ensemble le diff, les tests d’acceptation, les sorties des vérifications et les risques restants. Si Codex crée une PR en brouillon avec votre autorisation, vérifiez qu’elle utilise la bonne base et la bonne révision source. Si les outils de création de PR sont indisponibles, utilisez le diff et la description fournis pour préparer la PR selon votre procédure habituelle. Listez dans la PR les vérifications en suspens et gardez-la en brouillon tant que ces points restent ouverts.
-
Traitez les commentaires de revue et suivez votre procédure habituelle de fusion. Dans la même tâche, transmettez à Codex les commentaires précis à traiter, puis relancez les vérifications concernées. Avant la fusion, la personne chargée de la revue vérifie les résultats de CI requis et approuve le commit final.
Tests d’acceptation à exiger
Ces tests délimitent suffisamment le ticket pour que vous puissiez le déléguer et en examiner le résultat. Adaptez leurs noms à votre outil de test tout en conservant le comportement vérifié par chacun.
Parcours stable
Préparez cinq événements fixes : evt_105 a l’horodatage le plus récent, evt_104, evt_103 et evt_102 partagent l’horodatage intermédiaire, et evt_101 a le plus ancien.
Avec limit=2, exigez exactement les pages suivantes : [evt_105, evt_104], [evt_103, evt_102], [evt_101]. Les événements de même horodatage doivent se répartir entre la première et la deuxième page.
Vérifiez l’ordre exact, l’absence d’identifiants en double ou manquants, la présence d’un curseur non nul sur chacune des deux premières pages et d’un curseur nul sur la troisième. Faites échouer le test si le parcours dépasse trois requêtes.
Réutilisez le premier curseur de continuation avec la même limite, sans modifier les données de test. Vérifiez que la deuxième page contient les mêmes éléments et indique la même position de reprise.
Taille par défaut et valeurs limites
Préparez plus de 100 événements. Vérifiez que la réponse contient 25 éléments sans paramètre limit, un élément avec limit=1 et 100 éléments avec limit=100.
Limites invalides
Vérifiez que 0, 101, -1, une valeur fractionnaire et une chaîne telle que many donnent lieu à une réponse HTTP 400 conforme à la structure existante des erreurs de validation.
Pages vides et dernières pages
Vérifiez qu’un projet vide auquel l’accès est autorisé renvoie items: [] et next_cursor: null. Avec limit=2, vérifiez que cinq événements produisent une troisième page d’un élément avec un curseur nul et que quatre événements produisent une deuxième page de deux éléments avec un curseur nul. Une dernière page pleine ne doit pas imposer de requête supplémentaire pour obtenir une page vide.
Authentification
Vérifiez qu’en l’absence de session ou avec une session invalide, la réponse respecte le schéma 401 convenu, même si la requête contient un curseur mal formé ou associé à un autre périmètre. La validation du curseur ne doit pas prendre le pas sur l’authentification.
Isolation des tenants
Authentifiez-vous en tant que tenant A, puis demandez un projet du tenant B ou un projet inexistant. Vérifiez que les deux requêtes renvoient le même corps de réponse 404 générique convenu, sans données d’événements, même avec un curseur mal formé ou associé à un autre périmètre. Le contrôle d’accès doit précéder la validation du curseur.
Portée du curseur
Demandez un projet accessible du tenant A avec (1) un curseur signé valide provenant d’un autre projet du tenant A et (2) un curseur signé valide émis pour le tenant B.
Vérifiez que les deux requêtes renvoient le même corps de réponse HTTP 400 invalid_cursor, sans données d’événements ni détails sur la portée du curseur. Testez ces cas séparément des requêtes visant un projet inaccessible, qui doivent renvoyer un statut 404.
Curseurs mal formés ou altérés
Sur un projet accessible, vérifiez que l’API renvoie une réponse HTTP 400 invalid_cursor pour les tokens que la fonction de gestion des curseurs ne peut pas décoder, ceux dont la signature a été modifiée et les tokens signés dans lesquels un champ obligatoire du curseur est absent ou de type incorrect. Utilisez des jeux de test isolés pour la signature.
Testez les versions non prises en charge ou les tokens expirés si la fonction partagée gère ces cas. Vérifiez que les réponses respectent le schéma d’erreur générique convenu et ne contiennent ni données décodées, ni identifiants, ni erreurs brutes, ni traces de pile.
Comportement existant
L’exemple teste la route existante qui renvoie les détails d’un projet avec des requêtes autorisées, des requêtes non autorisées et des requêtes visant un projet inaccessible. Vérifiez la syntaxe des fichiers .mjs de l’exemple. Pour votre service, exécutez la suite habituelle de tests de non-régression, la vérification des types et le lint.
Schéma public des réponses
Validez les corps des réponses HTTP 200, 400, 401 et 404 effectivement reçues à l’aide des schémas OpenAPI convenus. Vérifiez la présence des clés obligatoires, le type des champs de chaque élément, le format de l’horodatage, la possibilité pour le curseur de valoir null et l’absence de propriétés supplémentaires à tous les niveaux des objets.
Ajoutez des champs internes aux événements stockés et vérifiez qu’ils sont absents des éléments sérialisés. Vérifiez également que chaque réponse d’erreur contient uniquement le code et le message génériques approuvés.
Documentation
Vérifiez que l’opération OpenAPI et ses exemples décrivent fidèlement les paramètres implémentés, les schémas exacts de succès et d’erreur, la priorité du contrôle d’accès, l’ordre de tri, les règles de dernière page et les limites des garanties de répétabilité et de cohérence sur des données de test fixes. Si le dépôt prévoit une validation de la spécification d’API, exécutez-la.
Résultats attendus
Compte rendu commun des vérifications
Utilisez le format suivant pour chaque critère d’acceptation. Renseignez-le à partir des résultats observés. Si un nouveau test n’existait pas lors des vérifications initiales, indiquez « pas encore présent » pour le distinguer d’une vérification existante qui a échoué.
Critère / test : [comportement précis à vérifier et emplacement du test]
Environnement : [référence de la configuration publiée, versions de l’environnement d’exécution, de la base de données et des dépendances]
Avant : [révision d’origine et identifiant du diff portant uniquement sur les tests, commande, code de sortie, résultat observé de l’assertion, chemin du journal]
Après : [révision finale ou révision de base accompagnée de l’identifiant du diff, commande, code de sortie, résultat observé de l’assertion, chemin du journal]
État du test : [réussi / échoué / bloqué / non exécuté]
Limites / action à mener lors de la revue : [comportement non testé, point bloquant ou décision à prendre]
Associez chaque exécution, réussie ou non, à l’état réellement testé, y compris les modifications portant uniquement sur les tests appliquées à la révision d’origine. Joignez les journaux, les données de réponse utilisées pour les tests et le diff final aux livrables. Les noms de fichiers et de tests ne suffisent pas à prouver l’exécution. Actualisez le compte rendu lorsque la révision testée change. Une vérification bloquée ou non exécutée ne peut pas être comptée comme réussie.
Une description de PR utile associe chaque affirmation à un résultat vérifiable. Utilisez ce modèle et remplacez chaque champ entre crochets par les résultats observés.
Titre : Ajout de la pagination par curseur pour les événements d’un projet
Ticket : API-142
Base : [branche et commit]
État testé : [commit en tête de la branche source, ou commit de base accompagné de l’identifiant du diff joint]
Modifications
- Ajout du point de terminaison des événements du projet à l’aide de la fonction existante de contrôle d’accès au projet.
- Réutilisation des curseurs signés dont la portée est limitée au tenant et au projet.
- Ajout de tests de contrat et mise à jour de l’opération OpenAPI.
Résultats des tests d’acceptation
- [Pour chaque critère ci-dessus, indiquez sur une ligne le test, le résultat observé et la référence du journal.]
Validation
- Avant modification : [commande, résultat et référence du journal pour chaque vérification initiale].
- Nouveaux tests sur le code d’origine : [résultat ou, si les tests n’ont pas été exécutés, raison de leur non-exécution].
- Après modification : [commande, résultat et référence du journal pour chaque vérification requise].
- CI : [liens vers les vérifications et états constatés, ou mention de leur indisponibilité].
- Vérifications restantes : [vérifications à effectuer et points bloquants. S’il n’en reste aucune, indiquez-le et joignez les résultats qui le confirment].
Points à examiner lors de la revue
- [Comportement présentant le plus de risques et critères restant à vérifier.]
Risques restants et retour en arrière
- [Indiquez les limites observées et distinguez-les des hypothèses non vérifiées.]
- [Expliquez comment annuler la modification et précisez si des appelants en dépendent.]
Traitez les commentaires de revue
Pour traiter un point précis soulevé lors de la revue, formulez une demande de suivi ciblée. Par exemple :
Traitez ce commentaire de revue dans la modification en cours :
« Répétez le parcours des cinq événements avec limit=3. Vérifiez que la première page contient
[evt_105, evt_104, evt_103], que la deuxième contient [evt_102, evt_101] et que le curseur de cette dernière est nul. »
Ajoutez le test de non-régression. Ne modifiez l’implémentation que si ce test met en évidence un défaut.
Relancez les tests ciblés du point de terminaison ainsi que toutes les autres vérifications requises
concernées par la modification. Mettez à jour le tableau des vérifications en y indiquant l’état testé
et les résultats observés. Conservez le même périmètre. Ne fusionnez rien et ne déployez rien.
Si Codex ne peut pas consulter directement la revue, fournissez-lui le texte du commentaire. La personne chargée de la revue doit vérifier que la réponse corrige bien le comportement signalé. Une réponse qui reprend simplement les termes du commentaire ne suffit pas.
Quand interrompre le travail
- Les vérifications initiales échouent ou ne peuvent pas être exécutées. Indiquez la vérification en échec et les détails de configuration. Corrigez l’environnement ou convenez d’un état de référence documenté avant de poursuivre.
- Le contrat entre en conflit avec le comportement existant. Précisez le conflit, les appelants concernés et la décision minimale à prendre. Ne modifiez pas le contrat sans le signaler.
- La modification exige une migration, une nouvelle dépendance ou une refonte de l’authentification ou des autorisations. Expliquez pourquoi, puis scindez le ticket ou redéfinissez son périmètre avant de l’élargir.
- Des tests échouent après l’implémentation. Ne présentez pas la modification comme prête à être fusionnée. Signalez les cas en échec. Ne supprimez pas d’assertions et n’affaiblissez pas les contrôles d’accès pour faire réussir les vérifications.
- L’intégration permettant d’écrire dans le dépôt est indisponible. Fournissez le diff, la proposition de description de PR et les résultats des vérifications. Un membre de l’équipe pourra finaliser la PR dans le dépôt.
Réutilisez le workflow
Une fois le premier résultat accepté lors de la revue, enregistrez les informations à fournir et la liste des critères d’acceptation dans le modèle de ticket de votre équipe. Consignez les instructions durables propres au dépôt, notamment les commandes de test et l’emplacement des contrats d’API, dans AGENTS.md ou dans le guide de contribution existant.
Related use cases
Créer des jeux par navigateur
Décrivez votre jeu à Codex pour qu’il en tire un plan précis, puis développe une version...
Créer une CLI que Codex peut utiliser
Demandez à Codex de créer une CLI qu’il pourra exécuter depuis n’importe quel dossier...
Enquêtez sur un incident et testez un correctif
Fournissez à Codex Cloud un dossier d’incident expurgé des données sensibles et un...