Codex use case
Convierte un issue en un pull request con cambios probados
Delega la implementación de una funcionalidad pequeña con criterios de aceptación y revisa la evidencia.
Dale a Codex un issue de alcance definido, un entorno en la nube ya preparado y una lista explícita de verificaciones. Obtén un cambio listo para revisar, con la implementación, las pruebas, la documentación y la evidencia de que se cumple cada criterio de aceptación.
Best for
- Ingenieros de startups que implementan cambios en una API a partir de un issue de alcance definido.
- Revisores que necesitan examinar la implementación junto con la evidencia de validación.
- Equipos que cuentan con criterios de aceptación y un entorno de pruebas operativo.
Contents
Convierte un issue en un pull request con cambios probados
Delega la implementación de una funcionalidad pequeña con criterios de aceptación y revisa la evidencia.
Dale a Codex un issue de alcance definido, un entorno en la nube ya preparado y una lista explícita de verificaciones. Obtén un cambio listo para revisar, con la implementación, las pruebas, la documentación y la evidencia de que se cumple cada criterio de aceptación.
Dale a Codex un issue de alcance definido, un entorno en la nube ya preparado y una lista explícita de verificaciones. Obtén un cambio listo para revisar, con la implementación, las pruebas, la documentación y la evidencia de que se cumple cada criterio de aceptación.
Best for
- Ingenieros de startups que implementan cambios en una API a partir de un issue de alcance definido.
- Revisores que necesitan examinar la implementación junto con la evidencia de validación.
- Equipos que cuentan con criterios de aceptación y un entorno de pruebas operativo.
Starter prompt
Asigna a Codex un issue pequeño y bien definido. Pídele que entregue un cambio que alguien de tu equipo pueda revisar, con la implementación, las pruebas, la documentación y la evidencia de que funciona como se pidió. Realiza toda la tarea en un mismo entorno en la nube ya preparado para que el desarrollo y la validación usen la misma configuración del repositorio.
-
En la aplicación web o de escritorio, elige Trabajar en > Nube y selecciona un entorno publicado.
-
Para crear uno, selecciona Crear entorno, elige los repositorios de GitHub y pide a Codex que prepare el entorno de ejecución, las dependencias y los servicios de prueba aislados.
-
Revisa el informe de configuración, corrige los problemas que hicieron fallar las verificaciones y selecciona Publicar antes de iniciar una tarea nueva.
- Sigue la guía vigente de entornos en la nube.
- Mantén las instrucciones del repositorio en AGENTS.md.
Si cambian las dependencias o la configuración de los servicios, edita el entorno reutilizable, pruébalo y selecciona Volver a publicar. Luego verifica los cambios en una tarea nueva.
Al actualizar el repositorio, no se vuelven a ejecutar los comandos de instalación ni de inicio. Las tareas existentes conservan su propio estado.
Configura por separado los destinos de red necesarios y las credenciales de prueba. Permitir un destino no concede acceso al servicio.
Si no puedes ejecutar las verificaciones obligatorias, completa la configuración antes de usar este flujo de trabajo.
Cuándo usarlo
Elige una funcionalidad con un contrato por escrito y un criterio claro para darla por terminada, como un nuevo punto de acceso de la API, una regla de validación o una pequeña ampliación de un servicio existente. El issue debe poder resolverse en un solo pull request y tener verificaciones que se puedan ejecutar sin acceso a producción.
El ejemplo agrega paginación al historial de actividad de un proyecto. El ejemplo ejecutable en Node.js incluye autenticación, acceso a proyectos, cursores firmados y una ruta existente para consultar los detalles de un proyecto. Usa registros de prueba fijos en memoria, así que puedes verificar el contrato de paginación sin una base de datos ni un servicio externo.
Prueba el ejemplo ejecutable
Descarga el ejemplo, descomprímelo y abre la carpeta issue-to-tested-pr. Usa Node 22.6 o posterior. El ejemplo usa la biblioteca estándar y no requiere instalar paquetes ni tener una clave de API.
node --version
node api.mjs
node test.mjs --seeded
node test.mjs
node reproduce.mjs ./evidence
node api.mjs inicia un servidor HTTP local, solicita la primera página, imprime los ID públicos de sus eventos y cierra el servidor.
La ejecución de las pruebas con --seeded termina deliberadamente con un código distinto de cero. Al continuar solo a partir de la marca de tiempo, se omiten dos eventos que tienen la misma marca de tiempo que el último evento de la primera página. Las mismas pruebas pasan con la corrección, que usa el par de marca de tiempo e ID para continuar.
El comando de reproducción solo termina con éxito si las pruebas fallan en el estado con el defecto introducido y pasan en el estado corregido.
Con limit=2, el recorrido corregido devuelve estas páginas en tres solicitudes:
[evt_105, evt_104]
[evt_103, evt_102]
[evt_101]
Las dos primeras páginas tienen un cursor de continuación. La última tiene next_cursor: null.
El estado con el defecto devuelve [evt_105, evt_104] y luego [evt_101], por lo que se pierden evt_103 y evt_102.
En una ejecución exitosa, las 36 pruebas pasan en el estado corregido. En el estado con el defecto fallan tres aserciones que ponen de manifiesto el problema de paginación.
Para verificar tu entorno, coloca los archivos en el repositorio que elegiste y prepara y publica un entorno con una versión compatible de Node. Luego ejecuta los comandos en una tarea nueva en la nube y registra los resultados.
reproduce.mjs guarda red.log, green.log, observed-http.json y run-manifest.json en el directorio indicado para la evidencia. El archivo de manifiesto identifica el entorno de ejecución, los comandos, los códigos de salida, el defecto introducido, la corrección y los hashes SHA-256 de los archivos fuente.
No registra tokens de sesión ni la clave de firma. Consulta README.md y openapi.json para conocer la configuración del ejemplo y su contrato público.
Examina el ejemplo ejecutable
Estos fragmentos provienen de los archivos de la descarga. Ejecuta los archivos completos después de descomprimir el ZIP.
Implementación del cursor y del orden de los eventos
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)))
),
};
}Registros precargados y datos de prueba para el acceso a proyectos
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,
};
}Contrato HTTP y pruebas de regresión
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"
);
});Recopilación de evidencia
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
)
);Prepara la información de entrada
Un enlace al issue solo sirve si Codex puede leerlo. Si la fuente no está disponible, pega o adjunta el contenido pertinente. Incluye la siguiente información junto con el prompt inicial.
| Información | Issue de ejemplo |
|---|---|
| Issue y resultado esperado | API-142: agrega GET /v1/projects/{project_id}/events para que los clientes puedan recorrer el historial de actividad por páginas. |
| Repositorio y base | Coloca la carpeta del ejemplo en el repositorio que elegiste. Luego registra la rama y el commit exacto que seleccionaste para la tarea. |
| Código pertinente | Los archivos api.mjs, fixtures.mjs, test.mjs y openapi.json del ejemplo, además del archivo AGENTS.md de tu repositorio. |
| Patrones existentes | Mantén la autenticación y la autorización de acceso al proyecto antes de la validación de la paginación. Usa cursores firmados y la estructura pública documentada para los errores. Al adaptar el issue, sigue los patrones de las funciones auxiliares de tu repositorio. |
| Configuración de las pruebas | Proyectos de prueba fijos en memoria para dos tenants, seis proyectos del tenant A, eventos con la misma marca de tiempo repartidos entre dos páginas, marcas de tiempo con precisión fraccionaria, páginas finales vacías y páginas finales con exactamente el máximo de elementos, y 105 eventos para verificar los límites. |
| Validación | node test.mjs --seeded (fallo esperado), node test.mjs (estado corregido) y node reproduce.mjs ./evidence. Ejecuta node --check para cada archivo .mjs. Para tu propio servicio, agrega los comandos que usa para la verificación de tipos, el lint, las pruebas de regresión y la CI. |
| Alcance | El punto de acceso, sus pruebas específicas y su documentación de OpenAPI. Quedan fuera las dependencias nuevas, la migración de la base de datos, el frontend, el despliegue y el rediseño de la autenticación o la autorización. |
| Entrega | Un borrador de PR si la integración disponible con el repositorio lo permite y autorizaste su creación. En caso contrario, un diff y un borrador de la descripción del PR. |
Para el ejemplo, prepara Node 22.6 o posterior. Las pruebas crean sus propias sesiones de prueba y su propia clave para firmar cursores, y vinculan un servidor HTTP temporal a la interfaz de loopback. Para tu propio servicio, prepara también sus dependencias, su configuración de pruebas y sus servicios aislados. Antes de asignar este issue, verifica que la función auxiliar de cursores pueda vincular un cursor con el tenant autenticado y el proyecto solicitado.
Define el contrato de la API
Antes de implementar el cambio, confirma que el siguiente contrato del ejemplo coincide con el de tu repositorio. Una respuesta exitosa tiene el estado HTTP 200 y contiene exactamente { items, next_cursor }.
Cada elemento tiene exactamente cuatro campos públicos obligatorios: id (cadena no vacía), created_at (cadena con una marca de tiempo UTC en formato RFC 3339), type (cadena no vacía) y summary (cadena). items es un arreglo de estos objetos. next_cursor es una cadena opaca no vacía o null.
Las claves internas del tenant y del proyecto, los metadatos de la base de datos, el contenido sin procesar de los eventos y las credenciales no forman parte de la respuesta. Especifica en OpenAPI los tipos de los campos de respuesta, cuáles son obligatorios, cuáles admiten null y la estructura de los objetos que no admiten propiedades adicionales. Verifica que los cuerpos HTTP serializados cumplan ese esquema.
Los eventos se ordenan por created_at DESC, id DESC. Se conserva la precisión fraccionaria de la marca de tiempo y los ID siguen un orden lexicográfico descendente. El cursor reanuda el recorrido estrictamente después de la última tupla devuelta según ese orden.
El tamaño de página predeterminado es 25. Los límites válidos son números enteros decimales del 1 al 100, ambos inclusive. No se puede repetir el parámetro de límite ni el de cursor. Devuelve un cursor no nulo solo si existe otro evento que cumpla los criterios.
Tanto una página final parcial como una que contiene exactamente el máximo de elementos devuelven next_cursor: null. Un proyecto vacío al que se tiene acceso devuelve items: [] y next_cursor: null.
El ejemplo firma cada cursor y lo vincula con el tenant autenticado y el proyecto solicitado. Verifica la autenticación y autoriza el acceso al proyecto solicitado antes de validar el límite o el cursor. Así, los errores de parámetros no pueden revelar si existe un proyecto de otro tenant.
Si la sesión falta o no es válida, la respuesta es 401. Si el proyecto es inaccesible o no existe, la respuesta es el mismo error 404 genérico. Después de confirmar el acceso, verifica el límite antes que el cursor.
Si el cursor está mal formado, fue manipulado, tiene una estructura no válida o no corresponde al tenant o al proyecto, la respuesta es HTTP 400 con el código invalid_cursor. Si se envía a un proyecto autorizado un cursor válido para otro proyecto del mismo tenant o para otro tenant, ese cursor se considera no válido.
Enviar un cursor no debe cambiar el resultado de la autorización para un proyecto inaccesible. El cursor de la versión 1 del ejemplo no caduca. Las versiones no compatibles se consideran no válidas.
En el ejemplo, cada cuerpo de error contiene exactamente { error: { code, message } }. Ambos campos son obligatorios y de tipo cadena, y no se admiten campos adicionales.
Usa estas respuestas de error genéricas:
- 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.
Excluye de todos los cuerpos de error el contenido decodificado del cursor, los identificadores de tenant y proyecto, los errores sin procesar y las trazas de pila. Si el repositorio real usa otra estructura u otros códigos aprobados, usa los de ese repositorio. Resuelve esa diferencia de contrato antes de cambiar las funciones auxiliares compartidas.
Las aserciones de recorrido y repetición usan datos de prueba fijos sin escrituras concurrentes. Al seguir los cursores, cada evento se devuelve una sola vez en el orden acordado. El recorrido termina en un máximo de ceil(event_count / limit) solicitudes si el historial contiene eventos, o en una solicitud si está vacío.
Mientras los datos de prueba permanezcan sin cambios, volver a usar un cursor válido con los mismos parámetros de solicitud devuelve los mismos elementos y la misma posición de continuación. Los tokens firmados que se generan no tienen que ser idénticos byte por byte.
Este contrato no garantiza cursores de un solo uso, paginación sobre una instantánea ni resultados idénticos después de que cambie el historial. Documenta la política de consistencia que el repositorio ya aplica al historial. Si no existe una política y el issue requiere paginación sobre una instantánea, detente hasta que se tome una decisión de producto.
Cómo usar este flujo de trabajo
-
Elige el issue y prepara el entorno. Confirma que el contrato anterior coincide con el del repositorio real. Selecciona el entorno en la nube que preparaste y el commit base previsto. Incluye la información del issue directamente en la tarea. Para iniciar este flujo no necesitas un disparador automático del sistema de seguimiento de issues.
-
Ejecuta el prompt inicial. Reemplaza los campos entre corchetes con los datos del repositorio, la información del issue, los límites del alcance y las verificaciones. Elige si Codex debe devolver un diff o crear un borrador de PR con tu autorización. El prompt inicial de esta página contiene las instrucciones completas para la entrega.
-
Verifica el estado inicial. Antes de editar, Codex debe leer las instrucciones del repositorio y ejecutar las verificaciones iniciales aprobadas.
Para el ejemplo, ejecuta
node test.mjsy las verificaciones de sintaxis deREADME.md. La opción--seededsirve para demostrar por separado un fallo deliberado.Para tu propio servicio, ejecuta las verificaciones existentes de regresión, tipos y lint. Es posible que aún no exista el archivo de pruebas específicas del nuevo punto de acceso. En ese caso, registra la prueba como pendiente de creación. Resuelve los fallos en las verificaciones existentes o los problemas de disponibilidad de los servicios de prueba antes de atribuir resultados posteriores a la nueva funcionalidad.
-
Implementa y verifica el contrato. Pide a Codex que use las funciones auxiliares existentes para el acceso y los cursores, implemente el punto de acceso, agregue las pruebas de aceptación que se indican más adelante y actualice la documentación de la API. Cuando sea viable, las pruebas nuevas deben fallar con el código original y pasar después del cambio. Guarda esos resultados con el registro de verificación final. En el ejemplo descargable,
--seededactiva un defecto deliberado para hacer esta comparación. Esto es independiente de los posibles fallos de las verificaciones iniciales en tu repositorio. -
Revisa el cambio y la evidencia. Revisa en conjunto el diff, las verificaciones de aceptación, sus resultados y los riesgos pendientes. Si Codex crea un borrador de PR autorizado, verifica que tanto la base como el origen de los cambios sean correctos. Si las herramientas de PR no están disponibles, usa el diff y la descripción que recibiste para preparar el PR según tu proceso habitual. Enumera en el borrador de PR las verificaciones pendientes y mantenlo como borrador mientras sigan sin resolverse.
-
Atiende los comentarios de la revisión e integra el cambio según tu proceso habitual. En la misma tarea, indica a Codex qué comentarios debe atender. Luego vuelve a ejecutar las verificaciones afectadas. Antes de integrar el cambio, la persona que lo revisa confirma que se cumplan los requisitos de CI y aprueba el commit final.
Pruebas de aceptación obligatorias
Estas pruebas delimitan el issue para que puedas delegarlo y definen con precisión lo que se debe revisar. Adapta los nombres a tu herramienta de pruebas y conserva el comportamiento que demuestra cada prueba.
Recorrido estable
Carga cinco eventos fijos: evt_105 tiene la marca de tiempo más reciente, evt_104, evt_103 y evt_102 comparten la marca de tiempo intermedia, y evt_101 tiene la más antigua.
Con limit=2, exige exactamente estas páginas: [evt_105, evt_104], [evt_103, evt_102], [evt_101]. Los eventos con la misma marca de tiempo deben quedar repartidos entre la primera página y la segunda.
Comprueba que el orden sea exacto, que no falten ni se repitan ID, que los cursores de las dos primeras páginas no sean nulos y que el de la tercera sí lo sea. Haz que la prueba falle si el recorrido supera las tres solicitudes.
Vuelve a usar el primer cursor de continuación con el mismo límite y sin cambiar los datos de prueba. Comprueba que la segunda página tenga los mismos elementos y la misma posición de continuación.
Tamaño predeterminado y valores límite
Carga más de 100 eventos. Comprueba que se devuelvan 25 elementos sin el parámetro limit, uno con limit=1 y 100 con limit=100.
Límites no válidos
Comprueba que se devuelva HTTP 400 para 0, 101, -1, un valor fraccionario y una cadena como many, con la estructura existente para los errores de validación.
Páginas vacías y finales
Comprueba que un proyecto vacío al que se tiene acceso devuelva items: [] y next_cursor: null. Con limit=2, comprueba que cinco eventos produzcan una tercera página con un elemento y un cursor nulo, y que cuatro eventos produzcan una segunda página con dos elementos y un cursor nulo. Cuando la última página esté llena, no debe hacer falta otra solicitud para obtener una página vacía.
Autenticación
Comprueba que las solicitudes sin sesión o con una sesión no válida reciban un 401 con el esquema acordado, incluso cuando incluyan un cursor mal formado o de otro ámbito. La validación del cursor no debe prevalecer sobre la autenticación.
Aislamiento entre tenants
Autentícate como el tenant A y solicita un proyecto del tenant B o un proyecto inexistente. Comprueba que en ambos casos se devuelva el mismo cuerpo genérico acordado para el error 404, sin datos de eventos, incluso si la solicitud incluye un cursor mal formado o de otro ámbito. La autorización debe preceder a la validación del cursor.
Ámbito del cursor
Solicita un proyecto del tenant A al que tengas acceso con (1) un cursor firmado válido de otro proyecto del tenant A y (2) un cursor firmado válido emitido para el tenant B.
Comprueba que ambas solicitudes devuelvan HTTP 400 con el mismo cuerpo de error invalid_cursor, sin datos de eventos ni detalles sobre el ámbito. Prueba estos casos por separado de las solicitudes a un proyecto inaccesible, que deben devolver 404.
Cursores mal formados o manipulados
En un proyecto al que tengas acceso, comprueba que se devuelva HTTP 400 con invalid_cursor para los tokens que la función auxiliar no puede decodificar, los tokens con firmas modificadas y los tokens firmados que omiten campos obligatorios del cursor o que asignan a esos campos valores de un tipo incorrecto. Usa datos de firma aislados para las pruebas.
Incluye versiones no compatibles o tokens caducados si la función auxiliar compartida admite esas características. Comprueba que las respuestas sigan el esquema genérico de error acordado y que no contengan datos decodificados, identificadores, errores sin procesar ni trazas de pila.
Comportamiento existente
El ejemplo comprueba la ruta existente de detalles del proyecto con solicitudes autorizadas, solicitudes no autorizadas y solicitudes a proyectos inaccesibles. Ejecuta las verificaciones de sintaxis de sus archivos .mjs. En tu servicio, ejecuta el conjunto habitual de pruebas de regresión, la verificación de tipos y el lint.
Esquema de la respuesta pública
Valida los cuerpos reales de las respuestas HTTP 200, 400, 401 y 404 con los esquemas de OpenAPI acordados. Comprueba las claves obligatorias, los tipos de los campos de cada elemento, el formato de la marca de tiempo, si el cursor admite null y la ausencia de propiedades adicionales en todos los niveles de los objetos.
Carga eventos que incluyan campos internos y verifica que esos campos no aparezcan en los elementos serializados. Verifica que cada respuesta de error contenga únicamente el código y el mensaje genéricos aprobados.
Documentación
Verifica que la operación de OpenAPI y sus ejemplos coincidan con los parámetros implementados, los esquemas exactos de las respuestas exitosas y de error, la prioridad de la autorización, el orden de los eventos, las reglas para la última página y los límites de repetición y consistencia con datos de prueba fijos. Si el repositorio tiene una validación de la especificación de la API, ejecútala.
Qué debe incluir la entrega
Registro compartido de evidencia
Usa este formato para registrar la evidencia de cada criterio de aceptación. Complétalo con los resultados observados. Si una prueba nueva aún no existe en el estado inicial, regístrala como “aún no existe”. Reserva el estado “fallida” para una verificación que se ejecutó y falló.
Criterio / prueba: [comportamiento específico y ubicación de la prueba]
Entorno: [referencia a la configuración publicada y versiones del entorno de ejecución, de la base de datos y de las dependencias]
Antes: [revisión original e ID del diff que solo modifica las pruebas, comando, código de salida, resultado observado de la aserción y ruta del registro]
Después: [revisión final o revisión base más el ID del diff, comando, código de salida, resultado observado de la aserción y ruta del registro]
Estado: [aprobada / fallida / bloqueada / sin ejecutar]
Limitaciones / acción de quien revisa: [comportamiento sin probar, impedimento o decisión necesaria]
Vincula las ejecuciones fallidas y exitosas con los estados exactos que se probaron, incluidos los cambios limitados a las pruebas que se aplicaron a la revisión original. Incluye los registros, los datos de prueba de las respuestas y el diff final en la entrega. Los nombres de los archivos y de las pruebas por sí solos no demuestran que se hayan ejecutado. Actualiza la evidencia cuando cambie la revisión probada. Las verificaciones bloqueadas o que no se hayan ejecutado no cuentan como aprobadas.
Una descripción de PR útil vincula cada afirmación con la evidencia que la respalda. Usa esta plantilla de entrega y reemplaza todos los campos entre corchetes con los resultados observados.
Título: agregar paginación con cursores a los eventos del proyecto
Issue: API-142
Base: [rama y commit]
Estado probado: [commit de la rama de origen o commit base más el identificador del diff adjunto]
Cambios
- Agrega el punto de acceso para los eventos del proyecto con la función auxiliar existente de acceso al proyecto.
- Reutiliza cursores firmados vinculados al tenant y al proyecto.
- Agrega pruebas del contrato y actualiza la operación de OpenAPI.
Evidencia de aceptación
- [Una fila por cada criterio anterior con la prueba, el resultado observado y la referencia al registro.]
Validación
- Antes de los cambios: [comando, resultado y referencia al registro de cada verificación inicial].
- Pruebas nuevas con el código original: [resultado o indicación de que no se ejecutaron y el motivo].
- Después de los cambios: [comando, resultado y referencia al registro de cada verificación obligatoria].
- CI: [enlaces a las verificaciones y sus estados reales, o indicación de que la CI no está disponible].
- Verificaciones pendientes: [verificaciones concretas y qué impide completarlas, o evidencia de que no queda ninguna pendiente].
Prioridades de la revisión
- [Comportamiento de mayor riesgo y criterios pendientes de resolución.]
Riesgos pendientes y reversión
- [Limitaciones observadas. Distínguelas de los supuestos que aún no se han comprobado.]
- [Cómo revertir el cambio y si hay código que dependa de él.]
Ajusta el cambio a partir de los comentarios de la revisión
Da una instrucción de seguimiento centrada en un hallazgo concreto de la revisión. Por ejemplo:
Atiende este comentario de la revisión en el cambio actual:
“Repite el recorrido de los cinco eventos con limit=3. Comprueba que la primera página contenga
[evt_105, evt_104, evt_103], que la segunda contenga [evt_102, evt_101] y que el cursor de continuación de la segunda sea nulo.”
Agrega la prueba de regresión. Modifica la implementación solo si esa prueba revela un fallo.
Vuelve a ejecutar las pruebas específicas del punto de acceso y las demás verificaciones obligatorias
que se vean afectadas por el cambio. Actualiza la tabla de evidencia con el estado probado y los resultados observados.
Mantén el mismo alcance. No integres ni despliegues el cambio.
Si Codex no puede acceder directamente a la revisión, proporciona el texto del comentario. Quien revise el cambio debe comprobar que se haya corregido el comportamiento señalado, aunque la respuesta ya cumpla al pie de la letra lo que dice el comentario.
Cuándo detenerte
- Las verificaciones iniciales fallan o no se pueden ejecutar. Devuelve la verificación que falló y los detalles de la configuración. Corrige el entorno o acuerda y documenta un estado inicial antes de continuar.
- El contrato entra en conflicto con el comportamiento existente. Identifica el conflicto, el código afectado que usa esa funcionalidad y la decisión mínima necesaria. No cambies el contrato sin señalarlo.
- El cambio requiere una migración, una dependencia nueva o un rediseño de la autenticación o la autorización. Explica el motivo y divide el issue o redefine su alcance antes de ampliarlo.
- Las pruebas fallan después de la implementación. No des el cambio por listo para integrarse. Informa qué casos fallaron. No elimines aserciones ni debilites la autorización para que pasen las verificaciones.
- No está disponible la integración que permite escribir en el repositorio. Devuelve el diff, el borrador de la descripción del PR y la evidencia de validación. Alguien de tu equipo puede completar la entrega en el repositorio.
Reutiliza el flujo de trabajo
Una vez que la persona que revisa el cambio acepte el primer resultado, guarda la información del issue y la lista de verificación de los criterios de aceptación en la plantilla de issues de tu equipo. Registra en AGENTS.md o en la guía de contribución existente las instrucciones del repositorio que se mantienen estables, como los comandos de prueba y la ubicación de los contratos de la API.
Related use cases
Crea juegos de navegador
Usa Codex para convertir la descripción de tu juego en un plan bien definido y luego...
Crea una CLI que Codex pueda usar
Pídele a Codex que cree una CLI que pueda ejecutar desde cualquier carpeta, combinar con...
Investigar un incidente y probar una corrección
Dale a Codex Cloud la documentación del incidente sin datos sensibles y un entorno de...