For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme
Codex

Codex use case

이슈를 구현하고 테스트를 거친 pull request 만들기

작은 기능의 구현을 맡길 때 인수 기준을 함께 제시하고, 각 기준을 충족했는지 검증 근거를 확인하세요.

Difficulty Intermediate
Time horizon Long-running

Codex에 범위가 명확한 이슈와 미리 준비한 클라우드 환경을 제공하고 어떤 검사를 실행할지 지정하세요. 구현 코드, 테스트, 문서를 받아 각 인수 기준의 검증 근거와 함께 변경 사항을 검토할 수 있습니다.

Best for

  • 범위가 명확한 API 이슈를 구현하는 스타트업 엔지니어
  • 구현 내용과 검증 근거를 함께 확인해야 하는 리뷰어
  • 인수 기준이 정해져 있고 테스트를 실행할 환경을 갖춘 팀

Contents

    ← All use cases

    이슈를 구현하고 테스트를 거친 pull request 만들기

    작은 기능의 구현을 맡길 때 인수 기준을 함께 제시하고, 각 기준을 충족했는지 검증 근거를 확인하세요.

    Codex에 범위가 명확한 이슈와 미리 준비한 클라우드 환경을 제공하고 어떤 검사를 실행할지 지정하세요. 구현 코드, 테스트, 문서를 받아 각 인수 기준의 검증 근거와 함께 변경 사항을 검토할 수 있습니다.

    Intermediate
    Long-running

    Codex에 범위가 명확한 이슈와 미리 준비한 클라우드 환경을 제공하고 어떤 검사를 실행할지 지정하세요. 구현 코드, 테스트, 문서를 받아 각 인수 기준의 검증 근거와 함께 변경 사항을 검토할 수 있습니다.

    Intermediate
    Long-running

    Best for

    • 범위가 명확한 API 이슈를 구현하는 스타트업 엔지니어
    • 구현 내용과 검증 근거를 함께 확인해야 하는 리뷰어
    • 인수 기준이 정해져 있고 테스트를 실행할 환경을 갖춘 팀

    Starter prompt

    미리 준비된 클라우드 환경 중 내가 이 작업에 사용하도록 선택한 환경을 사용하세요. [repository]에서 [base branch and commit]을 기준으로 [issue ID and title]을 구현하세요. 입력 자료: [paste or attach the issue, acceptance criteria, relevant files, public success/error schemas, authorization and pagination contract, test fixtures, and approved validation commands]. 리포지터리 지침: [AGENTS.md and any relevant nested instructions]. 작업 범위: [files or component boundaries]. 제외 대상: [explicit exclusions]. 필수 검사: [focused tests, relevant regression tests, type check, lint, and any required CI checks]. 결과물: [create a draft PR in this repository if the available integration supports it and I authorize it here / return a diff and draft PR description]. 먼저 지침과 관련 코드를 읽으세요. 시작 커밋을 확인하고 이 환경에서 사용할 수 있는 실행 도구로 기존 검사를 실행해 변경 전 상태를 확인하세요. 새로 작성해야 할 테스트 파일은 작성 예정으로 기록하세요. 변경 전 검사가 실패하거나 필요한 접근 권한이 없거나 필수 검사를 실행할 수 없다면, 구현을 시작하기 전에 무엇이 작업을 막고 있는지 정확히 보고하세요. 간단한 계획을 제시한 뒤 인수 기준을 충족하는 데 필요한 최소한의 변경만 적용하세요. 인증·인가, 오류 처리, 페이지네이션, 테스트에는 기존 패턴을 따르세요. 기존 동작과 요구된 동작을 구분할 수 있는 테스트를 추가하세요. 이 페이지네이션 예제에서는 타임스탬프가 같은 항목들이 페이지 경계 양쪽에 나뉘는 경우를 테스트하세요. 고정된 픽스처로 정해진 한도 안에서 순회를 마치고 같은 순회를 재현할 수 있는지 확인하세요. 마지막 페이지의 항목 수가 페이지 크기보다 적은 경우와 정확히 일치하는 경우를 모두 테스트하세요. 커서를 검증하기 전에 권한을 확인하는지, 잘못된 커서를 어떻게 처리하는지, 성공 응답과 오류 응답이 각각의 스키마를 따르는지도 검증하세요. 관련 API 문서를 수정해 실제로 구현한 공개 API 응답과 정확히 일치시키세요. 구현을 마치면 필수 검사를 다시 실행하세요. 각 인수 기준을 어떤 테스트나 근거로 검증했는지 명시하세요. 실행한 명령어와 결과, 테스트한 커밋 또는 작업 트리 상태, 실행하지 않은 검사를 기록하세요. 변경 전부터 실패하던 검사와 변경 후 새로 실패한 검사를 구분하세요. 실행하지 않은 검사를 통과했다고 보고하지 마세요. diff 또는 작성을 승인받은 초안 PR을 제출하세요. 간결한 변경 요약, 인수 기준별 검증 근거를 정리한 표, 남은 위험 요소, 리뷰어가 해야 할 일도 함께 제공하세요. 병합하거나 배포하지 마세요. 프로덕션 데이터 사용, 권한 변경, 새 종속성 추가, 합의한 범위 확대도 하지 마세요. API 계약이 리포지터리의 내용과 충돌하거나 스키마 변경이 필요하면 작업을 멈추고 결정을 요청하세요.
    미리 준비된 클라우드 환경 중 내가 이 작업에 사용하도록 선택한 환경을 사용하세요. [repository]에서 [base branch and commit]을 기준으로 [issue ID and title]을 구현하세요. 입력 자료: [paste or attach the issue, acceptance criteria, relevant files, public success/error schemas, authorization and pagination contract, test fixtures, and approved validation commands]. 리포지터리 지침: [AGENTS.md and any relevant nested instructions]. 작업 범위: [files or component boundaries]. 제외 대상: [explicit exclusions]. 필수 검사: [focused tests, relevant regression tests, type check, lint, and any required CI checks]. 결과물: [create a draft PR in this repository if the available integration supports it and I authorize it here / return a diff and draft PR description]. 먼저 지침과 관련 코드를 읽으세요. 시작 커밋을 확인하고 이 환경에서 사용할 수 있는 실행 도구로 기존 검사를 실행해 변경 전 상태를 확인하세요. 새로 작성해야 할 테스트 파일은 작성 예정으로 기록하세요. 변경 전 검사가 실패하거나 필요한 접근 권한이 없거나 필수 검사를 실행할 수 없다면, 구현을 시작하기 전에 무엇이 작업을 막고 있는지 정확히 보고하세요. 간단한 계획을 제시한 뒤 인수 기준을 충족하는 데 필요한 최소한의 변경만 적용하세요. 인증·인가, 오류 처리, 페이지네이션, 테스트에는 기존 패턴을 따르세요. 기존 동작과 요구된 동작을 구분할 수 있는 테스트를 추가하세요. 이 페이지네이션 예제에서는 타임스탬프가 같은 항목들이 페이지 경계 양쪽에 나뉘는 경우를 테스트하세요. 고정된 픽스처로 정해진 한도 안에서 순회를 마치고 같은 순회를 재현할 수 있는지 확인하세요. 마지막 페이지의 항목 수가 페이지 크기보다 적은 경우와 정확히 일치하는 경우를 모두 테스트하세요. 커서를 검증하기 전에 권한을 확인하는지, 잘못된 커서를 어떻게 처리하는지, 성공 응답과 오류 응답이 각각의 스키마를 따르는지도 검증하세요. 관련 API 문서를 수정해 실제로 구현한 공개 API 응답과 정확히 일치시키세요. 구현을 마치면 필수 검사를 다시 실행하세요. 각 인수 기준을 어떤 테스트나 근거로 검증했는지 명시하세요. 실행한 명령어와 결과, 테스트한 커밋 또는 작업 트리 상태, 실행하지 않은 검사를 기록하세요. 변경 전부터 실패하던 검사와 변경 후 새로 실패한 검사를 구분하세요. 실행하지 않은 검사를 통과했다고 보고하지 마세요. diff 또는 작성을 승인받은 초안 PR을 제출하세요. 간결한 변경 요약, 인수 기준별 검증 근거를 정리한 표, 남은 위험 요소, 리뷰어가 해야 할 일도 함께 제공하세요. 병합하거나 배포하지 마세요. 프로덕션 데이터 사용, 권한 변경, 새 종속성 추가, 합의한 범위 확대도 하지 마세요. API 계약이 리포지터리의 내용과 충돌하거나 스키마 변경이 필요하면 작업을 멈추고 결정을 요청하세요.

    Codex에 범위가 작고 요구 사항이 명확한 이슈를 맡기세요. 팀원이 검토할 수 있도록 구현 코드, 테스트, 문서와 함께 요청한 동작을 확인한 결과도 제공해 달라고 하세요. 코드 작성과 검증에 동일한 리포지터리 설정을 사용하도록 미리 준비한 클라우드 환경 하나에서 작업을 진행하세요.

    1. 웹 또는 데스크톱 앱에서 작업 위치 > 클라우드를 선택한 다음 게시된 환경을 선택하세요.

    2. 새 환경이 필요하면 환경 만들기를 선택하고 GitHub 리포지터리를 고른 다음, Codex에 런타임과 종속성, 격리된 테스트 서비스를 준비해 달라고 하세요.

    3. 설정 보고서를 검토하고 실패한 검사의 원인을 해결한 다음, 새 작업을 시작하기 전에 게시를 선택하세요.

    종속성이나 서비스 설정이 바뀌면 재사용할 환경을 수정하고 테스트하세요. 다시 게시를 선택한 다음 새 작업에서 변경 사항을 확인하세요.

    리포지터리를 새로 고쳐도 설치 및 시작 명령어는 다시 실행되지 않으며, 기존 작업은 각자의 상태를 유지합니다.

    접속에 필요한 네트워크 대상과 테스트 자격 증명은 별도로 설정하세요. 네트워크 대상을 허용해도 서비스 접근 권한이 부여되지는 않습니다.

    필수 검사를 실행할 수 없다면 설정을 완료한 뒤 이 워크플로우를 사용하세요.

    언제 사용하면 좋을까요?

    새 API 엔드포인트, 유효성 검사 규칙, 기존 서비스에 추가할 작은 기능처럼 동작 명세가 문서화되어 있고 완료 기준이 명확한 기능을 고르세요. 하나의 pull request로 처리할 수 있고 프로덕션 환경에 접근하지 않고도 검증할 수 있는 이슈가 적합합니다.

    이 예시에서는 프로젝트 활동 피드에 페이지네이션을 추가합니다. 실행 가능한 Node.js 샘플에는 인증, 프로젝트 접근 제어, 서명된 커서, 기존 프로젝트 상세 조회 라우트가 포함되어 있습니다. 메모리에 고정된 테스트 레코드를 사용하므로 데이터베이스나 외부 서비스 없이 페이지네이션 명세를 검증할 수 있습니다.

    샘플 실행하기

    샘플을 다운로드한 다음 압축을 풀고 issue-to-tested-pr 폴더를 여세요. Node 22.6 이상을 사용하세요. 샘플은 표준 라이브러리를 사용하므로 패키지를 설치하거나 API 키를 준비할 필요가 없습니다.

    node --version
    node api.mjs
    node test.mjs --seeded
    node test.mjs
    node reproduce.mjs ./evidence

    node api.mjs는 로컬 HTTP 서버를 시작하고 첫 페이지를 요청한 뒤, 반환된 이벤트의 공개 ID를 출력하고 서버를 종료합니다.

    --seeded로 테스트를 실행하면 의도적으로 0이 아닌 종료 코드가 반환됩니다. 타임스탬프만 기준으로 다음 페이지를 조회해 첫 페이지의 마지막 이벤트와 타임스탬프가 같은 이벤트 두 개를 건너뛰기 때문입니다. 타임스탬프와 ID로 구성된 튜플을 기준으로 다음 페이지를 조회하도록 수정하면 같은 테스트를 통과합니다.

    재현 명령어는 결함을 심어 둔 상태에서 테스트가 실패하고 수정된 상태에서 통과할 때만 성공합니다.

    수정된 상태에서 limit=2로 조회하면 세 번의 요청으로 다음 페이지를 순서대로 받습니다.

    [evt_105, evt_104]
    [evt_103, evt_102]
    [evt_101]

    처음 두 페이지에는 다음 페이지를 조회할 커서가 있고 마지막 페이지에는 next_cursor: null이 반환됩니다.

    결함을 심어 둔 상태에서는 [evt_105, evt_104] 다음에 [evt_101]이 반환되어 evt_103과 evt_102가 누락됩니다.

    재현에 성공하면 수정된 상태에서 테스트 36개가 모두 통과합니다. 결함을 심어 둔 상태에서는 페이지네이션 결함을 드러내는 단언문 세 개가 실패합니다.

    환경을 검증하려면 선택한 리포지터리에 파일을 넣고 지원되는 Node 런타임을 갖춘 환경을 준비해 게시하세요. 그런 다음 새 클라우드 작업에서 명령어를 실행하고 결과를 기록하세요.

    reproduce.mjs는 지정한 검증 자료 디렉터리에 red.log, green.log, observed-http.json, run-manifest.json을 저장합니다. 매니페스트에는 런타임, 명령어, 종료 코드, 의도적으로 심은 결함과 수정 내용, 소스 파일의 SHA-256 해시가 기록됩니다.

    세션 토큰이나 서명 키는 기록하지 않습니다. 샘플 설정 방법과 공개 API 명세는 README.md와 openapi.json을 참고하세요.

    샘플 코드 살펴보기

    아래 코드는 다운로드한 파일에서 발췌한 것입니다. ZIP 파일의 압축을 푼 뒤 전체 파일을 실행하세요.

    커서 및 정렬 구현
    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)))
          ),
      };
    }
    미리 채워 둔 레코드와 프로젝트 접근 제어 테스트 픽스처
    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,
      };
    }
    HTTP 명세 및 회귀 테스트
    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"
      );
    });
    검증 자료 수집
    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
      )
    );

    작업에 필요한 자료 준비하기

    이슈 링크를 제공하려면 Codex가 해당 링크의 내용을 읽을 수 있어야 합니다. Codex가 원문에 접근할 수 없다면 관련 내용을 붙여 넣거나 첨부하세요. 시작 프롬프트에 다음 정보를 함께 제공하세요.

    입력 항목 예시 이슈
    이슈와 목표 API-142: 고객이 활동 피드를 페이지 단위로 조회할 수 있도록 GET /v1/projects/{project_id}/events를 추가합니다.
    리포지터리와 기준 커밋 선택한 리포지터리에 샘플 폴더를 넣고 작업 기준으로 선택한 브랜치와 정확한 커밋을 기록합니다.
    관련 코드 샘플의 api.mjs, fixtures.mjs, test.mjs, openapi.json과 리포지터리의 AGENTS.md를 제공합니다.
    기존 구현 패턴 페이지네이션 매개변수를 검증하기 전에 요청자를 인증하고 프로젝트 접근 권한을 확인합니다. 서명된 커서와 문서에 명시된 공개 오류 응답 형식을 사용합니다. 이슈를 실제 서비스에 맞게 조정할 때는 리포지터리의 기존 헬퍼를 따릅니다.
    테스트 설정 두 테넌트의 고정 인메모리 테스트 프로젝트(테넌트 A의 프로젝트 6개 포함), 타임스탬프가 같으면서 페이지 경계 양쪽에 걸쳐 있는 이벤트, 초 미만 단위까지 표현한 타임스탬프, 비어 있는 마지막 페이지와 정확히 꽉 찬 마지막 페이지, 조회 한도 검사용 이벤트 105개를 준비합니다.
    검증 node test.mjs --seeded(실패 예상), node test.mjs(수정된 상태), node reproduce.mjs ./evidence를 실행합니다. node --check로 각 .mjs 파일을 검사합니다. 실제 서비스에 적용할 때는 해당 서비스에서 사용하는 타입 검사, 린트, 회귀 테스트, CI 명령어를 추가합니다.
    범위 엔드포인트와 해당 기능의 테스트, OpenAPI 문서로 한정합니다. 새 종속성, 데이터베이스 마이그레이션, 프런트엔드, 배포, 인증·권한 체계 재설계는 제외합니다.
    인계 결과물 사용 가능한 리포지터리 연동이 초안 PR 생성을 지원하고 사용자가 Codex에 생성을 허가했다면 초안 PR을 제공합니다. 그 외에는 diff와 PR 설명 초안을 제공합니다.

    샘플에는 Node 22.6 이상을 준비하세요. 테스트는 자체 테스트 세션과 커서 서명 키를 생성하고 임시 HTTP 서버를 루프백 인터페이스에 바인딩합니다. 실제 서비스에서는 종속성, 테스트 설정, 격리된 서비스도 준비하세요. 이슈를 맡기기 전에 해당 서비스의 커서 헬퍼가 커서의 적용 범위를 인증된 테넌트와 요청된 프로젝트로 한정할 수 있는지 확인하세요.

    API 명세 정의하기

    구현에 앞서 아래 샘플 명세가 리포지터리에 맞는지 확인하세요. 성공 응답은 HTTP 200이며 본문은 정확히 { items, next_cursor } 형태여야 합니다.

    각 항목에는 필수 공개 필드가 정확히 네 개 있습니다. id는 비어 있지 않은 문자열, created_at은 UTC 기준 RFC 3339 형식의 타임스탬프 문자열, type은 비어 있지 않은 문자열, summary는 문자열입니다. items는 이 객체들로 이루어진 배열이며, next_cursor는 내부 구조가 공개되지 않은 비어 있지 않은 문자열이거나 null입니다.

    응답에는 내부 테넌트 및 프로젝트 키, 데이터베이스 메타데이터, 원본 이벤트 페이로드, 자격 증명을 포함하지 않습니다. OpenAPI에 응답 필드의 타입, 필수 필드, null 허용 여부, 객체에 명시된 필드 외에는 허용하지 않는다는 제약을 기술하세요. 직렬화된 HTTP 응답 본문이 이 스키마에 맞는지 검증하세요.

    이벤트는 created_at DESC, id DESC 순으로 정렬합니다. 타임스탬프는 초 미만 단위의 정밀도를 유지하고 ID는 문자열의 사전식 내림차순으로 정렬합니다. 커서로 조회를 재개하면 이 정렬 순서에서 마지막으로 반환한 튜플 바로 다음부터 반환합니다.

    기본 페이지 크기는 25이며, 조회 한도에는 1부터 100까지의 10진 정수만 허용합니다. 한 요청에 limit 또는 cursor 매개변수를 중복 지정하면 유효하지 않습니다. 조건에 맞는 이벤트가 더 있을 때만 null이 아닌 커서를 반환합니다.

    마지막 페이지가 일부만 채워졌든 정확히 꽉 찼든 next_cursor: null을 반환합니다. 접근 권한이 있는 프로젝트에 이벤트가 없으면 items: []와 next_cursor: null을 반환합니다.

    샘플은 각 커서에 서명하고 해당 커서의 적용 범위를 인증된 테넌트와 요청된 프로젝트로 한정합니다. 조회 한도나 커서를 검증하기 전에 요청자를 인증하고 요청된 프로젝트에 대한 접근 권한을 확인하세요. 그래야 매개변수 오류를 통해 다른 테넌트의 프로젝트 존재 여부가 드러나지 않습니다.

    세션이 없거나 유효하지 않으면 401을 반환합니다. 접근할 수 없는 프로젝트와 존재하지 않는 프로젝트에는 동일한 일반 404 응답을 반환합니다. 접근 권한을 확인한 뒤에는 커서보다 조회 한도를 먼저 검사하세요.

    커서의 형식이 잘못되었거나, 변조되었거나, 구조가 유효하지 않거나, 테넌트 또는 프로젝트가 일치하지 않으면 HTTP 400과 invalid_cursor 코드를 반환합니다. 같은 테넌트의 다른 프로젝트에서 유효한 커서나 다른 테넌트에서 유효한 커서도 접근 권한이 있는 현재 프로젝트의 요청에 사용하면 유효하지 않습니다.

    접근할 수 없는 프로젝트에 대한 권한 확인 결과는 요청에 포함된 커서에 따라 달라져서는 안 됩니다. 샘플의 버전 1 커서는 만료되지 않으며, 지원하지 않는 버전의 커서는 유효하지 않습니다.

    샘플의 모든 오류 본문은 정확히 { error: { code, message } } 형태입니다. 두 필드 모두 필수 문자열이며 추가 필드는 허용하지 않습니다.

    다음과 같은 일반 오류 응답을 사용하세요.

    • 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.

    어떤 오류 본문에도 디코딩한 커서 내용, 테넌트 및 프로젝트 식별자, 원본 오류, 스택 트레이스를 포함하지 마세요. 실제 리포지터리에서 승인한 오류 응답 형식이나 코드가 다르다면 해당 형식과 코드를 사용하세요. 공유 헬퍼를 변경하기 전에 명세 차이를 먼저 해결하세요.

    전체 순회와 재조회 테스트에는 동시 쓰기가 없는 고정 픽스처를 사용합니다. 커서를 따라가면 합의한 순서대로 모든 이벤트가 한 번씩 반환되어야 합니다. 비어 있지 않은 피드는 최대 ceil(event_count / limit)번의 요청으로, 빈 피드는 한 번의 요청으로 순회가 끝나야 합니다.

    픽스처가 바뀌지 않은 상태에서 같은 요청 매개변수와 유효한 커서로 다시 조회하면 같은 항목이 반환되고 다음 조회의 시작 위치도 같아야 합니다. 서명된 토큰까지 바이트 단위로 같을 필요는 없습니다.

    이 명세는 커서의 일회성 사용, 스냅샷 기반 페이지네이션, 피드 변경 후의 동일한 조회 결과를 보장하지 않습니다. 리포지터리의 기존 피드 일관성 정책을 문서화하세요. 정책이 없는데 이슈에서 스냅샷 동작을 요구한다면 작업을 멈추고 제품 차원의 결정을 받으세요.

    사용 방법

    1. 이슈를 선택하고 환경을 준비하세요. 위 명세가 실제 리포지터리에 맞는지 확인하세요. 준비한 클라우드 환경과 기준으로 삼을 커밋을 선택하세요. 작업에 이슈 자료를 직접 제공하세요. 이 워크플로우를 시작하는 데 이슈 트래커의 자동 트리거는 필요하지 않습니다.

    2. 시작 프롬프트를 실행하세요. 대괄호로 표시된 입력란을 리포지터리 정보, 이슈 자료, 작업 범위, 검사 항목으로 채우세요. Codex에 diff를 요청할지, 초안 PR을 만들도록 허가하고 생성을 요청할지 선택하세요. 이 페이지의 시작 프롬프트에는 전체 인계 지침이 포함되어 있습니다.

    3. 변경 전 기준 상태를 확인하세요. Codex는 수정하기 전에 리포지터리 지침을 읽고 승인된 검사를 실행해 기준 상태를 확인해야 합니다.

      샘플에서는 node test.mjs와 README.md의 구문 검사를 실행하세요. --seeded는 실패를 의도적으로 재현하는 별도 실행 옵션입니다.

      실제 서비스에서는 기존 회귀 테스트, 타입 검사, 린트를 실행하세요. 새 엔드포인트 전용 테스트 파일이 아직 없다면 새로 작성할 테스트로 기록하세요. 이후 결과가 새 기능 때문이라고 판단하기 전에 기존 검사 실패나 테스트 서비스 이용 불가 문제부터 해결하세요.

    4. 명세를 구현하고 검증하세요. Codex에 기존 접근 제어 및 커서 헬퍼를 사용해 엔드포인트를 구현하고, 아래 인수 테스트를 추가하고, 해당 API 문서를 업데이트해 달라고 하세요. 가능하면 새 테스트가 원래 코드에서 실패하고 변경 후에는 통과하는지 확인하세요. 두 결과를 최종 검증 기록과 함께 보관하세요. 다운로드한 샘플에서는 --seeded로 의도적인 결함을 활성화해 결과를 비교합니다. 이 비교는 실제 리포지터리에서 변경 전 기준 검사가 실패한 경우와 구분하세요.

    5. 변경 사항과 검증 자료를 검토하세요. diff, 인수 기준별 검사, 검사 출력, 남은 위험을 함께 검토하세요. Codex가 권한을 받아 초안 PR을 만들었다면 병합 대상(base)과 변경 소스(head)가 올바른지 확인하세요. PR 도구를 사용할 수 없다면 반환된 diff와 설명을 사용해 평소 절차에 따라 PR을 준비하세요. 해결되지 않은 검사는 모두 초안 PR에 기록하고, 해결할 때까지 병합 준비가 완료된 것으로 표시하지 마세요.

    6. 리뷰 의견을 반영하고 평소 절차에 따라 병합하세요. 같은 작업에서 Codex에 반영할 의견을 구체적으로 전달한 다음 영향을 받은 검사를 다시 실행하세요. 병합하기 전에 리뷰어가 필수 CI 결과를 확인하고 최종 커밋을 승인합니다.

    필수 인수 테스트

    다음 테스트를 기준으로 이슈의 범위를 위임할 수 있을 만큼 좁히고 검토할 동작을 구체화하세요. 각 테스트가 검증하는 동작은 유지하되 이름은 사용하는 테스트 러너에 맞게 바꾸세요.

    안정적인 전체 순회

    고정 이벤트 다섯 개를 준비하세요. evt_105의 타임스탬프가 가장 최근이고 evt_104, evt_103, evt_102는 동일한 중간 타임스탬프를 가지며 evt_101의 타임스탬프가 가장 오래되었습니다.

    limit=2로 조회했을 때 페이지가 정확히 [evt_105, evt_104], [evt_103, evt_102], [evt_101] 순으로 반환되어야 합니다. 타임스탬프가 같은 이벤트 그룹이 첫 페이지와 두 번째 페이지에 걸쳐 있어야 합니다.

    순서가 정확한지, 중복되거나 누락된 ID가 없는지, 처음 두 페이지의 커서는 null이 아니고 세 번째 페이지의 커서는 null인지 검증하세요. 전체 순회에 세 번을 초과하는 요청이 필요하면 테스트를 실패 처리하세요.

    픽스처를 변경하지 않고 첫 페이지에서 받은 커서와 같은 조회 한도로 다시 요청하세요. 두 번째 페이지의 항목과 다음 조회의 시작 위치가 이전과 같은지 검증하세요.

    기본 페이지 크기와 경곗값

    이벤트를 100개 넘게 준비하세요. limit 매개변수를 생략하면 25개, limit=1이면 1개, limit=100이면 100개의 항목이 반환되는지 검증하세요.

    유효하지 않은 조회 한도

    조회 한도가 0, 101, -1, 소수부가 있는 값 또는 many 같은 문자열이면 기존 유효성 검사 오류 형식으로 HTTP 400을 반환하는지 검증하세요.

    빈 페이지와 마지막 페이지

    접근 권한이 있는 빈 프로젝트에서 items: []와 next_cursor: null을 반환하는지 검증하세요. limit=2일 때 이벤트가 다섯 개면 세 번째 페이지에 항목 한 개와 null 커서가, 이벤트가 네 개면 두 번째 페이지에 항목 두 개와 null 커서가 반환되어야 합니다. 마지막 페이지가 꽉 찼더라도 빈 페이지를 확인하기 위한 추가 요청이 필요해서는 안 됩니다.

    인증

    세션이 없거나 유효하지 않으면 합의한 스키마에 맞는 401 응답을 반환하는지 검증하세요. 형식이 잘못된 커서나 다른 범위의 커서가 포함된 요청도 확인하세요. 커서 검증 결과가 인증 결과에 우선해서는 안 됩니다.

    테넌트 격리

    테넌트 A로 인증한 다음 테넌트 B의 프로젝트 또는 존재하지 않는 프로젝트를 요청하세요. 형식이 잘못되거나 범위가 일치하지 않는 커서를 요청에 포함한 경우에도 이벤트 데이터 없이 합의한 동일한 일반 404 본문을 반환하는지 검증하세요. 접근 권한을 먼저 확인한 뒤 커서를 검증해야 합니다.

    커서 적용 범위

    접근 권한이 있는 테넌트 A의 프로젝트에 두 가지 커서로 각각 요청하세요. (1) 테넌트 A의 다른 프로젝트에서 발급한 유효한 서명 커서와 (2) 테넌트 B에서 발급한 유효한 서명 커서를 사용하세요.

    두 요청 모두 이벤트 데이터나 적용 범위의 세부 정보 없이 동일한 HTTP 400 invalid_cursor 본문을 반환하는지 검증하세요. 접근할 수 없는 프로젝트에 대한 요청은 404를 반환해야 하므로 별도로 테스트하세요.

    형식이 잘못되거나 변조된 커서

    접근 권한이 있는 프로젝트에서 헬퍼가 디코딩할 수 없는 토큰, 서명이 변경된 토큰, 서명은 되어 있지만 필수 커서 필드가 누락되거나 필드 타입이 잘못된 토큰에 대해 HTTP 400 invalid_cursor를 반환하는지 검증하세요. 서명에는 격리된 테스트 픽스처를 사용하세요.

    공유 헬퍼가 버전 검사나 토큰 만료를 지원한다면 지원하지 않는 버전이나 만료된 토큰도 테스트하세요. 응답이 합의한 일반 오류 스키마와 일치하고 디코딩한 내용, 식별자, 원본 오류, 스택 트레이스를 포함하지 않는지 검증하세요.

    기존 동작

    샘플은 접근 권한이 있는 요청, 인증되지 않은 요청, 접근할 수 없는 프로젝트에 대한 요청으로 기존 프로젝트 상세 조회 라우트를 검사합니다. 샘플의 .mjs 파일에 대해 구문 검사를 실행하세요. 실제 서비스에서는 기존 회귀 테스트 모음, 타입 검사, 린트를 실행하세요.

    공개 응답 스키마

    실제 HTTP 200, 400, 401, 404 응답 본문을 합의한 OpenAPI 스키마와 대조해 검증하세요. 필수 키, 항목 필드의 타입, 타임스탬프 형식, 커서의 null 허용 여부를 확인하고 모든 중첩 수준에서 객체에 추가 속성이 없는지 검사하세요.

    저장된 테스트 이벤트에 내부 필드를 넣은 다음 직렬화된 항목에 이 필드가 없는지 검증하세요. 모든 오류 응답에는 승인된 일반 오류 코드와 메시지만 포함되어야 합니다.

    문서

    OpenAPI에 정의한 작업과 예시가 실제 구현과 일치하는지 확인하세요. 매개변수, 정확한 성공 및 오류 응답 스키마, 접근 권한 확인의 우선순위, 정렬 순서, 마지막 페이지 규칙, 고정 픽스처에서 재조회 결과와 일관성을 보장하는 범위를 검토하세요. 리포지터리에 API 명세 검증 절차가 있다면 실행하세요.

    결과물 작성 예시

    공유용 검증 기록

    각 인수 기준에 대해 아래 형식으로 실제 실행 결과를 기록하세요. 기준 상태에 새 테스트가 아직 없었다면 ‘아직 없음’으로 기록해 기존 검사가 실패한 경우와 구분하세요.

    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]

    실패한 실행과 통과한 실행 각각에 대해 실제 테스트한 코드 상태를 기록하세요. 원래 리비전에 테스트만 추가해 실행했다면 그 변경 사항도 포함하세요. 인계 자료에는 로그, 응답 픽스처, 최종 diff를 함께 보관하세요. 파일명과 테스트 이름만으로는 실행 여부를 확인할 수 없습니다. 테스트 대상 리비전이 바뀌면 검증 기록도 업데이트하세요. 차단되었거나 실행하지 않은 검사는 통과로 집계할 수 없습니다.

    PR 설명에는 각 결과를 뒷받침하는 검증 자료를 연결하세요. 다음 인계 템플릿에서 대괄호로 표시된 모든 필드를 실제 확인한 결과로 채우세요.

    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.]

    리뷰 의견 반영하기

    리뷰에서 지적된 사항에 초점을 맞춰 후속 작업을 요청하세요. 예를 들면 다음과 같습니다.

    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.

    Codex가 리뷰를 직접 열람할 수 없다면 의견의 원문을 제공하세요. 리뷰어는 답변이 의견의 문구에 부합하는지만 보지 말고 지적된 동작이 실제로 바로잡혔는지 확인해야 합니다.

    작업을 멈춰야 할 때

    • 기준 상태에서 검사가 실패하거나 실행되지 않는 경우. 실패한 검사와 설정 세부 정보를 반환하세요. 환경을 수정하거나 기준 상태를 합의해 문서화한 뒤 계속하세요.
    • 명세가 기존 동작과 충돌하는 경우. 충돌 내용, 영향을 받는 호출부, 진행에 꼭 필요한 결정 사항을 명시하세요. 알리지 않고 명세를 바꾸지 마세요.
    • 마이그레이션, 새 종속성 또는 인증·권한 체계 재설계가 필요한 경우. 이유를 설명하고 작업 범위를 넓히기 전에 이슈를 나누거나 범위를 다시 정하세요.
    • 구현 후 테스트가 실패하는 경우. 병합할 준비가 된 것으로 처리하지 마세요. 실패한 사례를 보고하세요. 검사를 통과시키기 위해 단언문을 제거하거나 접근 권한 검사를 완화하지 마세요.
    • 리포지터리 쓰기 연동을 사용할 수 없는 경우. diff, PR 설명 초안, 검증 자료를 반환하세요. 팀원이 리포지터리에서 인계 절차를 마무리할 수 있습니다.

    워크플로우 재사용하기

    리뷰어가 첫 결과를 승인하면 이슈 자료와 인수 기준 체크리스트를 팀의 이슈 템플릿에 저장하세요. 테스트 명령어와 API 명세의 위치처럼 지속적으로 적용할 리포지터리 지침은 AGENTS.md 또는 기존 기여자 가이드에 기록하세요.

    Related use cases