Codex use case
Transforme uma issue em um pull request testado
Delegue a implementação de uma pequena funcionalidade com critérios de aceitação e revise as evidências.
Forneça ao Codex uma issue com escopo delimitado, um ambiente de nuvem já preparado e uma lista explícita de verificações. Você recebe a implementação, os testes e a documentação em uma alteração pronta para revisão, com evidências para cada critério de aceitação.
Best for
- Profissionais de engenharia de startups que implementam alterações de API a partir de uma issue com escopo delimitado.
- Quem precisa revisar a implementação junto com as evidências de validação.
- Equipes com critérios de aceitação definidos e um ambiente de testes funcionando.
Contents
Transforme uma issue em um pull request testado
Delegue a implementação de uma pequena funcionalidade com critérios de aceitação e revise as evidências.
Forneça ao Codex uma issue com escopo delimitado, um ambiente de nuvem já preparado e uma lista explícita de verificações. Você recebe a implementação, os testes e a documentação em uma alteração pronta para revisão, com evidências para cada critério de aceitação.
Forneça ao Codex uma issue com escopo delimitado, um ambiente de nuvem já preparado e uma lista explícita de verificações. Você recebe a implementação, os testes e a documentação em uma alteração pronta para revisão, com evidências para cada critério de aceitação.
Best for
- Profissionais de engenharia de startups que implementam alterações de API a partir de uma issue com escopo delimitado.
- Quem precisa revisar a implementação junto com as evidências de validação.
- Equipes com critérios de aceitação definidos e um ambiente de testes funcionando.
Starter prompt
Passe ao Codex uma issue pequena e bem definida. Peça uma alteração que alguém da equipe possa revisar, com implementação, testes, documentação e evidências de que o comportamento solicitado funciona. Execute a tarefa em um único ambiente de nuvem já preparado para que a implementação e a validação usem a mesma configuração do repositório.
-
No aplicativo web ou para desktop, escolha Trabalhar em > Nuvem e selecione um ambiente publicado.
-
Para criar um ambiente, selecione Criar ambiente, escolha os repositórios do GitHub e peça ao Codex que prepare o ambiente de execução, as dependências e os serviços de teste isolados.
-
Revise o relatório de configuração, corrija as falhas nas verificações e selecione Publicar antes de iniciar uma nova tarefa.
- Siga o guia atualizado de ambientes de nuvem.
- Mantenha as orientações do repositório no AGENTS.md.
Se as dependências ou a configuração dos serviços mudarem, edite o ambiente reutilizável, teste-o e selecione Republicar. Depois, verifique as alterações em uma nova tarefa.
Atualizar o repositório não executa novamente os comandos de instalação e inicialização. As tarefas existentes mantêm seu próprio estado.
Configure separadamente os destinos de rede necessários e as credenciais de teste. Permitir conexões a um destino não concede acesso ao serviço.
Se não for possível executar as verificações obrigatórias, conclua a configuração antes de usar este fluxo de trabalho.
Quando usar
Escolha uma funcionalidade com um contrato por escrito e um critério claro de conclusão, como um novo endpoint de API, uma regra de validação ou uma pequena adição a um serviço existente. A issue deve caber em um único pull request e ter verificações que possam ser executadas sem acesso à produção.
O exemplo adiciona paginação ao feed de atividades de um projeto. O exemplo executável em Node.js inclui autenticação, controle de acesso aos projetos, cursores assinados e uma rota já existente para consultar os detalhes de um projeto. Ele usa registros de teste fixos em memória para que você possa verificar o contrato de paginação sem banco de dados nem serviço externo.
Execute o exemplo
Baixe o exemplo, extraia os arquivos e abra a pasta issue-to-tested-pr. Use Node 22.6 ou posterior. O exemplo usa a biblioteca padrão e dispensa a instalação de pacotes e o uso de uma chave de API.
node --version
node api.mjs
node test.mjs --seeded
node test.mjs
node reproduce.mjs ./evidence
node api.mjs inicia um servidor HTTP local, solicita a primeira página, imprime os IDs públicos dos itens e encerra o servidor.
A execução dos testes com --seeded termina de propósito com um código de saída diferente de zero. Ao usar apenas o timestamp para buscar a próxima página, a implementação pula dois eventos com o mesmo timestamp do último evento da primeira página. Os mesmos testes passam com a correção, que usa o par formado pelo timestamp e pelo ID para continuar a paginação.
O comando de reprodução só tem êxito se os testes falharem no estado com o defeito introduzido e passarem no estado corrigido.
Com a correção e limit=2, a consulta retorna estas páginas em três requisições:
[evt_105, evt_104]
[evt_103, evt_102]
[evt_101]
As duas primeiras páginas têm um cursor de continuação. A última tem next_cursor: null.
No estado com defeito, a consulta retorna [evt_105, evt_104] e depois [evt_101], omitindo evt_103 e evt_102.
Em uma execução bem-sucedida, os 36 testes passam no estado corrigido. No estado com defeito, três asserções falham e revelam o problema de paginação.
Para verificar seu ambiente, coloque os arquivos no repositório escolhido, prepare e publique um ambiente com uma versão compatível do Node, execute os comandos em uma nova tarefa da nuvem e registre os resultados.
reproduce.mjs salva red.log, green.log, observed-http.json e run-manifest.json no diretório de evidências indicado. O manifesto identifica o ambiente de execução, os comandos, os códigos de saída, o defeito introduzido, a correção e os hashes SHA-256 dos arquivos-fonte.
O manifesto não registra tokens de sessão nem a chave de assinatura. Consulte README.md e openapi.json para conhecer a configuração e o contrato público do exemplo.
Examine o exemplo executável
Os trechos a seguir vêm dos arquivos disponíveis para download. Execute os arquivos completos depois de extrair o ZIP.
Implementação do cursor e da ordenação
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 pré-carregados e dados de teste de acesso aos projetos
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 e testes de regressão
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"
);
});Coleta de evidências
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
)
);Prepare o material da issue
Um link para a issue só é útil se o Codex conseguir acessá-la. Se a fonte estiver indisponível, cole ou anexe o conteúdo relevante. Inclua as informações a seguir junto com o prompt inicial.
| Informação | Issue de exemplo |
|---|---|
| Issue e resultado esperado | API-142: adicionar GET /v1/projects/{project_id}/events para que os clientes possam percorrer um feed de atividades página por página. |
| Repositório e base | Coloque a pasta do exemplo no repositório escolhido. Depois, registre a branch e o commit exato selecionados para a tarefa. |
| Código relevante | Os arquivos api.mjs, fixtures.mjs, test.mjs e openapi.json do exemplo, além do AGENTS.md do seu repositório. |
| Padrões existentes | Autentique o solicitante e autorize o acesso ao projeto antes de validar os parâmetros de paginação. Use cursores assinados e a estrutura pública de erros documentada. Ao adaptar a issue, siga os padrões das funções auxiliares existentes no repositório. |
| Configuração dos testes | Projetos de teste fixos em memória para dois tenants, seis projetos do tenant A, eventos com o mesmo timestamp distribuídos entre duas páginas, timestamps com precisão fracionária, páginas finais vazias e páginas finais com o número exato de itens do limite, além de 105 eventos para verificar os limites. |
| Validação | node test.mjs --seeded (falha esperada), node test.mjs (estado corrigido) e node reproduce.mjs ./evidence. Execute node --check para cada arquivo .mjs. Para seu próprio serviço, inclua os comandos que ele usa para verificação de tipos, lint, testes de regressão e CI. |
| Escopo | O endpoint, seus testes específicos e sua documentação OpenAPI. Ficam fora do escopo novas dependências, migração de banco de dados, frontend, implantação e reformulação da autenticação ou da autorização. |
| Entrega | Um PR em rascunho se a integração disponível com o repositório oferecer esse recurso e você tiver autorizado a criação. Caso contrário, um diff e um rascunho da descrição do PR. |
Para executar o exemplo, prepare o Node 22.6 ou posterior. Os testes criam as próprias sessões de teste e a chave de assinatura dos cursores, além de iniciar um servidor HTTP temporário na interface de loopback. Para seu próprio serviço, prepare também as dependências, a configuração de teste e os serviços isolados. Antes de atribuir a issue, verifique se a função auxiliar de cursores consegue vincular um cursor ao tenant autenticado e ao projeto solicitado.
Defina o contrato da API
Antes de implementar, confirme que o contrato do exemplo abaixo corresponde ao do seu repositório. Uma resposta de sucesso tem status HTTP 200 e contém exatamente { items, next_cursor }.
Cada item tem exatamente quatro campos públicos obrigatórios: id (string não vazia), created_at (string com timestamp em UTC no formato RFC 3339), type (string não vazia) e summary (string). items é um array desses objetos. next_cursor é uma string opaca não vazia ou null.
Chaves internas de tenants e projetos, metadados do banco de dados, conteúdo bruto dos eventos e credenciais não fazem parte da resposta. Descreva na especificação OpenAPI os tipos dos campos da resposta, quais são obrigatórios, quais aceitam null e quais propriedades são permitidas em cada objeto fechado. Verifique se o corpo serializado de cada resposta HTTP corresponde a esse schema.
Os eventos seguem a ordem created_at DESC, id DESC, preservando a precisão fracionária do timestamp e usando a ordem lexicográfica decrescente dos IDs. O cursor retoma a consulta estritamente após a última tupla retornada nessa ordem.
O tamanho padrão da página é de 25 itens. Os limites válidos são números inteiros decimais de 1 a 100, inclusive. Repetir o parâmetro de limite ou de cursor torna a requisição inválida. Retorne um cursor não nulo somente se houver outro evento que atenda aos critérios da consulta.
Tanto uma página final parcial quanto uma página final com o número exato de itens do limite retornam next_cursor: null. Um projeto vazio ao qual o solicitante tem acesso retorna items: [] e next_cursor: null.
O exemplo assina cada cursor e o vincula ao tenant autenticado e ao projeto solicitado. Autentique o solicitante e autorize o acesso ao projeto antes de validar o limite ou o cursor. Assim, erros nos parâmetros não revelam se um projeto de outro tenant existe.
Uma sessão ausente ou inválida resulta em 401. Um projeto inacessível e um projeto inexistente resultam na mesma resposta 404 genérica. Depois de confirmar o acesso, verifique o limite antes do cursor.
Um cursor malformado, adulterado, com estrutura inválida ou que não corresponda ao tenant ou ao projeto resulta em HTTP 400 com o código invalid_cursor. Mesmo em uma requisição a um projeto autorizado, é inválido fornecer um cursor que seja válido para outro projeto do mesmo tenant ou para outro tenant.
Fornecer um cursor não pode mudar o resultado da autorização para um projeto inacessível. O cursor de versão 1 do exemplo não expira. Versões sem suporte são inválidas.
No exemplo, o corpo de cada resposta de erro contém exatamente { error: { code, message } }. Os dois campos são strings obrigatórias e não há campos adicionais.
Use estas respostas de erro 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.
Não inclua conteúdo decodificado dos cursores, identificadores de tenants ou projetos, erros brutos nem rastreamentos de pilha em nenhum corpo de erro. Se a estrutura e os códigos aprovados pelo repositório real forem diferentes, use os do repositório. Resolva essa diferença de contrato antes de alterar funções auxiliares compartilhadas.
As asserções de navegação e repetição usam dados de teste fixos, sem gravações simultâneas. Ao seguir os cursores, a consulta retorna cada evento uma única vez na ordem combinada e termina em no máximo ceil(event_count / limit) requisições para um feed não vazio ou em uma requisição para um feed vazio.
Reutilizar um cursor válido com os mesmos parâmetros de requisição retorna os mesmos itens e a mesma posição de continuação enquanto os dados de teste permanecerem inalterados. Os tokens assinados não precisam ser idênticos byte a byte.
Este contrato não garante cursores de uso único, paginação sobre um snapshot nem resultados idênticos após alterações no feed. Documente a política de consistência do feed já adotada pelo repositório. Se não houver uma política e a issue exigir paginação sobre um snapshot, pare e peça uma decisão de produto.
Como usar
-
Escolha a issue e prepare o ambiente. Confirme que o contrato acima corresponde ao do repositório real. Selecione o ambiente de nuvem preparado e o commit base desejado. Forneça o material da issue diretamente na tarefa. Para iniciar esse fluxo, não é necessário um acionamento automático pelo sistema de acompanhamento de issues.
-
Execute o prompt inicial. Substitua os campos entre colchetes pelo repositório, pelo material da issue, pelos limites de escopo e pelas verificações. Escolha se o Codex deve retornar um diff ou criar um PR em rascunho com sua autorização. O prompt inicial desta página contém todas as instruções para a entrega.
-
Verifique o estado inicial. O Codex deve ler as instruções do repositório e executar as verificações iniciais aprovadas antes de editar o código.
Para o exemplo, execute
node test.mjse as verificações de sintaxe doREADME.md. A opção--seededserve para demonstrar uma falha intencional em uma execução separada.Para seu próprio serviço, execute os testes de regressão e as verificações de tipos e de lint existentes. Se o arquivo de testes específicos do novo endpoint ainda não existir, registre que esses testes precisam ser criados. Corrija falhas nas verificações existentes e restabeleça os serviços de teste indisponíveis antes de atribuir resultados posteriores à nova funcionalidade.
-
Implemente e verifique o contrato. Peça ao Codex que use as funções auxiliares existentes de acesso e de cursores, implemente o endpoint, adicione os testes de aceitação abaixo e atualize a documentação da API correspondente. Sempre que viável, os novos testes devem falhar com o código original e passar após a alteração. Inclua esses resultados no registro final de verificação. No exemplo disponível para download,
--seededseleciona um defeito intencional para essa comparação. Isso é diferente de uma falha nas verificações iniciais do seu repositório. -
Revise a alteração e as evidências. Examine em conjunto o diff, as verificações de aceitação, suas saídas e os riscos restantes. Se o Codex criar um PR em rascunho com sua autorização, confira se as branches de destino e de origem estão corretas. Se as ferramentas de PR estiverem indisponíveis, use o diff e a descrição recebidos para preparar o PR pelo processo habitual. Liste as verificações pendentes no rascunho e mantenha o PR em rascunho enquanto houver pendências.
-
Atenda aos comentários da revisão e faça o merge pelo processo habitual. Na mesma tarefa, passe ao Codex os comentários específicos a resolver. Depois, execute novamente as verificações afetadas. Antes do merge, a pessoa responsável pela revisão confirma os resultados das verificações obrigatórias de CI e aprova o commit final.
Testes de aceitação obrigatórios
Estes testes delimitam a issue para que ela possa ser delegada e definem comportamentos específicos para a revisão. Adapte os nomes à sua ferramenta de testes, preservando o comportamento que cada teste comprova.
Navegação estável entre páginas
Prepare cinco eventos fixos. evt_105 tem o timestamp mais recente. evt_104, evt_103 e evt_102 compartilham o timestamp intermediário. evt_101 tem o mais antigo.
Com limit=2, exija exatamente estas páginas: [evt_105, evt_104], [evt_103, evt_102], [evt_101]. Os eventos com o mesmo timestamp devem ficar distribuídos entre a primeira e a segunda página.
Verifique se os IDs aparecem na ordem exata, sem repetições nem omissões, se os cursores das duas primeiras páginas são não nulos e se o da terceira é nulo. Faça o teste falhar se a consulta ultrapassar três requisições.
Reutilize o primeiro cursor de continuação com o mesmo limite e os mesmos dados de teste, sem alterá-los. Verifique se a segunda página contém os mesmos itens e indica a mesma posição de continuação.
Tamanho padrão e limites de página
Prepare mais de 100 eventos. Verifique se a consulta retorna 25 itens sem o parâmetro limit, um item com limit=1 e 100 itens com limit=100.
Limites inválidos
Verifique se 0, 101, -1, um valor fracionário e uma string como many resultam em HTTP 400 com a estrutura de erro de validação existente.
Páginas vazias e páginas finais
Verifique se um projeto vazio ao qual o solicitante tem acesso retorna items: [] e next_cursor: null. Com limit=2, verifique se cinco eventos produzem uma terceira página com um item e cursor nulo, e se quatro eventos produzem uma segunda página com dois itens e cursor nulo. Uma página final cheia não deve exigir outra requisição que retorne uma página vazia.
Autenticação
Verifique se, tanto na ausência de sessão quanto com uma sessão inválida, a resposta tem status 401 e segue o schema acordado, inclusive em requisições com um cursor malformado ou de outro escopo. A validação do cursor não pode se sobrepor à autenticação.
Isolamento entre tenants
Autentique-se como tenant A e solicite um projeto do tenant B ou um projeto inexistente. Verifique se ambos retornam o mesmo corpo genérico acordado para a resposta 404, sem dados de eventos, inclusive quando a requisição contém um cursor malformado ou de outro escopo. A autorização deve ocorrer antes da validação do cursor.
Escopo do cursor
Solicite um projeto do tenant A ao qual você tem acesso usando (1) um cursor assinado válido de outro projeto do tenant A e (2) um cursor assinado válido emitido para o tenant B.
Verifique se as duas requisições retornam o mesmo corpo de resposta HTTP 400 com invalid_cursor, sem dados de eventos nem detalhes do escopo. Teste esses casos separadamente das requisições a um projeto inacessível, que devem retornar 404.
Cursores malformados e adulterados
Em um projeto ao qual o solicitante tem acesso, verifique se a resposta é HTTP 400 com invalid_cursor para tokens que a função auxiliar não consegue decodificar, tokens com assinaturas modificadas e tokens assinados que omitem campos obrigatórios do cursor ou atribuem a eles valores do tipo incorreto. Use dados de assinatura isolados para os testes.
Inclua casos de versões sem suporte ou tokens expirados se a função auxiliar compartilhada der suporte a esses recursos. Verifique se as respostas correspondem ao schema de erro genérico acordado e não contêm conteúdo decodificado, identificadores, erros brutos nem rastreamentos de pilha.
Comportamento existente
O exemplo testa a rota existente de detalhes do projeto com requisições autorizadas, não autorizadas e direcionadas a projetos inacessíveis. Execute as verificações de sintaxe dos arquivos .mjs do exemplo. No seu serviço, execute a suíte de testes de regressão, a verificação de tipos e o lint já adotados.
Schema público das respostas
Valide os corpos reais das respostas HTTP 200, 400, 401 e 404 em relação aos schemas OpenAPI acordados. Verifique as chaves obrigatórias, os tipos dos campos de cada item, o formato do timestamp, se o cursor aceita null e se não há propriedades adicionais em nenhum nível dos objetos.
Inclua campos internos nos eventos armazenados e verifique se eles não aparecem nos itens serializados. Confira se cada resposta de erro contém apenas o código genérico e a mensagem aprovados.
Documentação
Verifique se a operação OpenAPI e os exemplos correspondem aos parâmetros implementados, aos schemas exatos de sucesso e erro, à precedência da autorização, à ordenação, às regras para a página final e aos limites de repetição e consistência com dados de teste fixos. Se o repositório tiver uma verificação da especificação da API, execute-a.
O que esperar da entrega
Registro compartilhado de evidências
Use este formato para registrar as evidências de cada critério de aceitação. Preencha-o com os resultados observados. Se um novo teste ainda não existia na verificação inicial, registre “ainda não existia” para diferenciá-lo de uma verificação existente que falhou.
Criterion / test: [specific behavior and test location]
Environment: [published setup reference; runtime, database and dependency versions]
Before: [revision + test-only diff ID; command; exit code; observed assertion; log path]
After: [final revision or base + diff ID; command; exit code; observed assertion; log path]
Status: [pass / fail / blocked / not run]
Limits / reviewer action: [untested behavior, blocker, or decision required]
Associe as execuções que falharam e as que passaram aos estados de fato testados, inclusive quando as alterações aplicadas à revisão original se limitaram aos testes. Entregue os logs, os dados de teste das respostas e o diff final. Só os nomes dos arquivos e dos testes não comprovam a execução. Atualize as evidências sempre que a revisão testada mudar. Verificações bloqueadas ou ainda não executadas não contam como aprovadas.
Uma boa descrição de PR associa cada afirmação à evidência correspondente. Use este modelo de entrega e substitua todos os campos entre colchetes pelos resultados observados.
Title: Add cursor pagination for project events
Issue: API-142
Base: [branch and commit]
Tested state: [head commit, or base commit plus attached diff identifier]
Changes
- Add the project-events endpoint using the existing project access helper.
- Reuse signed cursors scoped to tenant and project.
- Add contract tests and update the OpenAPI operation.
Acceptance evidence
- [One row per criterion above: test, observed result, and log reference.]
Validation
- Before edits: [command, result, and log reference for each baseline check].
- New tests against original code: [result, or not run with reason].
- After edits: [command, result, and log reference for each required check].
- CI: [check links and actual states, or not available].
- Remaining checks: [specific checks and blockers, or none with supporting evidence].
Reviewer focus
- [Highest-risk behavior and unresolved criteria.]
Remaining risk and rollback
- [Observed limitations; distinguish them from untested assumptions.]
- [How this change can be reverted and whether any callers depend on it.]
Faça ajustes a partir dos comentários da revisão
Para tratar um ponto específico da revisão, faça um pedido direcionado. Por exemplo:
Address this review comment in the current change:
"Repeat the five-event traversal with limit=3; assert pages
[evt_105, evt_104, evt_103] and [evt_102, evt_101], then a null cursor."
Add the regression test and change the implementation only if it exposes a
failure. Rerun the focused endpoint tests and any other required checks affected
by the edit. Update the evidence table with the tested state and actual results.
Keep the same scope. Do not merge or deploy.
Se o Codex não conseguir acessar a revisão diretamente, forneça o texto do comentário. A pessoa responsável pela revisão deve confirmar que a resposta corrige o comportamento apontado. Atender apenas à redação do comentário não basta.
Quando parar
- As verificações iniciais falham ou não podem ser executadas. Informe qual verificação falhou e os detalhes da configuração. Antes de continuar, corrija o ambiente ou chegue a um acordo sobre o estado inicial e documente-o.
- O contrato entra em conflito com o comportamento existente. Identifique o conflito, os consumidores afetados e a decisão mínima necessária. Não altere o contrato sem comunicar a mudança.
- A alteração exige uma migração, uma nova dependência ou a reformulação da autenticação ou da autorização. Explique o motivo e divida a issue ou redefina seu escopo antes de ampliá-lo.
- Os testes falham após a implementação. Não considere a alteração pronta para merge. Informe os casos que falharam. Não remova asserções nem enfraqueça as regras de autorização para que as verificações passem.
- A integração que permite gravar no repositório está indisponível. Entregue o diff, o rascunho da descrição do PR e as evidências de validação. Alguém da equipe pode concluir a entrega no repositório.
Reutilize o fluxo de trabalho
Depois que a pessoa responsável pela revisão aceitar o primeiro resultado, salve o material da issue e a lista de verificação de aceitação no modelo de issue da equipe. Registre as orientações estáveis do repositório, como os comandos de teste e a localização dos contratos da API, no AGENTS.md ou no guia de contribuição existente.
Related use cases
Crie jogos de navegador
Use o Codex para transformar a descrição do jogo em um plano bem definido e, em seguida, em...
Crie uma CLI que o Codex possa usar
Peça ao Codex que crie uma CLI que ele possa executar de qualquer pasta, combinar com...
Investigue um incidente e teste uma correção
Forneça ao Codex Cloud um dossiê do incidente sem dados sensíveis e um ambiente de teste...