Skip to content

Commit 2e07d98

Browse files
kwrkbclaude
andcommitted
fix: PR #2 レビュー指摘の修正
- session DELETE を 204 bodyless レスポンスに修正 - DALL-E 2 マスク方向を反転(境界辺を保持側に) - OPENAI_API_KEY 未設定時にコンストラクタで早期エラー - EventEmitter に setMaxListeners(0) を追加 - SSE controller.close() を try-catch で保護 - SESSION_SECRET 検証をモジュールトップレベルへ移動 - CI に SESSION_SECRET / DATABASE_URL 環境変数を追加 - expansion-panel.tsx の body インデントを修正 - AGENTS.md の Testing Guidelines を Vitest に更新 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2ff61fe commit 2e07d98

8 files changed

Lines changed: 35 additions & 23 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ jobs:
1515
- run: npx tsc --noEmit
1616
- run: npm run build
1717
- run: npm test
18+
env:
19+
SESSION_SECRET: test-secret-at-least-32-characters-long
20+
DATABASE_URL: file:./test.db

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
- Prefer `@/*` path alias imports over deep relative paths.
2626

2727
## Testing Guidelines
28-
- No dedicated test framework is configured yet.
28+
- Use Vitest for unit and integration tests. Run tests with `npm run test`.
2929
- Before opening a PR, run `npx tsc --noEmit` and `npm run build`.
3030
- Validate core flows manually: user setup, room creation, expansion run/adopt/reject, and lock behavior.
31-
- If adding tests, colocate near source as `*.test.ts` / `*.test.tsx` and document the run command in your PR.
31+
- Colocate tests near source as `*.test.ts` / `*.test.tsx`.
3232

3333
## Commit & Pull Request Guidelines
3434
- Follow concise, imperative commit messages; `fix: ...` style (Conventional Commit prefix) is preferred.

src/app/api/rooms/[id]/events/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ export async function GET(
3333
const onAbort = () => {
3434
clearInterval(keepAlive);
3535
unsubscribe();
36-
controller.close();
36+
try {
37+
controller.close();
38+
} catch {
39+
// already closed
40+
}
3741
};
3842

3943
req.signal.addEventListener("abort", onAbort, { once: true });

src/app/api/session/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export async function GET(req: NextRequest) {
1313
}
1414

1515
export async function DELETE() {
16-
const response = NextResponse.json(null, { status: 204 });
16+
const response = new NextResponse(null, { status: 204 });
1717
destroySession(response);
1818
return response;
1919
}

src/components/expansion/expansion-panel.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ export function ExpansionPanel({
6868
const expRes = await fetch(`/api/rooms/${roomId}/expansions`, {
6969
method: "POST",
7070
headers: { "Content-Type": "application/json" },
71-
body: JSON.stringify({
72-
fromTileId: fromTile.id,
73-
targetX,
74-
targetY,
75-
direction,
76-
promptJson: { text: promptText.trim() },
77-
}),
78-
});
71+
body: JSON.stringify({
72+
fromTileId: fromTile.id,
73+
targetX,
74+
targetY,
75+
direction,
76+
promptJson: { text: promptText.trim() },
77+
}),
78+
});
7979

8080
if (!expRes.ok) {
8181
const data = await expRes.json().catch(() => ({}));

src/lib/image-gen/dalle2-provider.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ function createMaskBuffer(size: number, direction: Direction): Buffer {
4040
const offset = (y * size + x) * channels;
4141
let shouldEdit = false;
4242

43-
if (direction === "E") shouldEdit = x >= size / 2;
44-
if (direction === "W") shouldEdit = x < size / 2;
45-
if (direction === "S") shouldEdit = y >= size / 2;
46-
if (direction === "N") shouldEdit = y < size / 2;
43+
// 透明部分(AIが生成する)は新タイルと接しない辺側。
44+
// 保持部分(不透明)は元タイルの境界辺側として残し、AIの継ぎ目生成に利用する。
45+
if (direction === "E") shouldEdit = x < size / 2;
46+
if (direction === "W") shouldEdit = x >= size / 2;
47+
if (direction === "S") shouldEdit = y < size / 2;
48+
if (direction === "N") shouldEdit = y >= size / 2;
4749

4850
if (shouldEdit) {
4951
data[offset] = 0;
@@ -66,9 +68,11 @@ export class DallE2ImageGenProvider implements ImageGenProvider {
6668
private client: OpenAI;
6769

6870
constructor() {
69-
this.client = new OpenAI({
70-
apiKey: process.env.OPENAI_API_KEY,
71-
});
71+
const apiKey = process.env.OPENAI_API_KEY;
72+
if (!apiKey) {
73+
throw new Error("OPENAI_API_KEY environment variable is not set");
74+
}
75+
this.client = new OpenAI({ apiKey });
7276
}
7377

7478
async generate(input: GenerateInput): Promise<GenerateOutput> {

src/lib/session.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@ export interface SessionUser {
99
displayName: string;
1010
}
1111

12-
function getSessionSecret(): string {
12+
const SESSION_SECRET = (() => {
1313
const secret = process.env.SESSION_SECRET;
1414
if (!secret || secret.length < 32) {
1515
throw new Error("SESSION_SECRET must be set and at least 32 characters long");
1616
}
1717
return secret;
18-
}
18+
})();
1919

2020
export async function getSession(req: NextRequest): Promise<SessionUser | null> {
2121
const sealed = req.cookies.get(SESSION_COOKIE_NAME)?.value;
2222
if (!sealed) return null;
2323

2424
try {
25-
return await unsealData<SessionUser>(sealed, { password: getSessionSecret() });
25+
return await unsealData<SessionUser>(sealed, { password: SESSION_SECRET });
2626
} catch {
2727
return null;
2828
}
2929
}
3030

3131
export async function setSession(res: NextResponse, user: SessionUser) {
3232
const sealed = await sealData(user, {
33-
password: getSessionSecret(),
33+
password: SESSION_SECRET,
3434
ttl: SESSION_TTL_SECONDS,
3535
});
3636

src/lib/sse-emitter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EventEmitter } from "events";
22

33
const emitter = new EventEmitter();
4+
emitter.setMaxListeners(0);
45

56
function roomEventKey(roomId: string) {
67
return `room:${roomId}`;

0 commit comments

Comments
 (0)