Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ Azure stack itself is `infra/azure/`.
provider** and have nothing to do with the retired runtime. Do not remove
those two, and do not add the rest back.

## Focused investigation and execution

Operate with a strict token budget. Before opening a file, determine whether it
is necessary. Locate relevant code with repository search and inspect only the
required ranges.

- Use `rg` for symbol/text discovery, limited-range readers for files, targeted
`git diff -- <files>`, and targeted test files or cases.
- Form a debugging hypothesis before gathering the minimum evidence needed to
confirm or reject it.
- Do not read whole large files, recursively inspect directories without a
reason, reread unchanged files, print full diffs where a targeted diff is
enough, run verbose tests unless debugging, explain routine actions, research
alternatives after finding a satisfactory implementation, or modify adjacent
code merely because it could be improved.
- Once the requested change is implemented and verified, stop.

## UI & Design (required skills)

Every change that touches the interface — pages, components, layout, spacing,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { experimental_upgradeWebSocket } from "@vercel/functions";
import { eq } from "drizzle-orm";

import { schema } from "@codev/db";

import {
handleGen2CollaborationSocket,
gen2CollaborationSocketMaxPayload,
} from "@/lib/gen2/collaboration-socket";
import { requireGen2Member } from "@/lib/gen2/workspaces";
import { apiError } from "@/lib/http/api";
import { withUser } from "@/lib/http/api-route";
import { getDatabase } from "@/lib/platform/database";

type Params = { workspaceId: string };

export const dynamic = "force-dynamic";
export const maxDuration = 300;

/** Authenticated browser-only transport for a Gen 2 shared document. */
export const GET = withUser<Params>(
async ({ user: sessionUser, params: { workspaceId } }) => {
const [membership, user] = await Promise.all([
requireGen2Member(workspaceId, sessionUser.id),
getDatabase()
.select({
id: schema.users.id,
login: schema.users.login,
name: schema.users.name,
avatarUrl: schema.users.avatarUrl,
})
.from(schema.users)
.where(eq(schema.users.id, sessionUser.id))
.limit(1)
.then((rows) => rows[0]),
]);
if (!user) return apiError(new Error("Workspace not found."), 404);
try {
return await experimental_upgradeWebSocket(
(socket) =>
handleGen2CollaborationSocket(workspaceId, socket, user, {
canEdit: membership.role !== "viewer",
}),
{ maxPayload: gen2CollaborationSocketMaxPayload },
);
} catch {
return apiError(
new Error("Realtime collaboration is temporarily unavailable."),
503,
);
}
},
{ errorStatus: 503 },
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getApiUser: vi.fn(),
create: vi.fn(),
move: vi.fn(),
remove: vi.fn(),
}));

vi.mock("@/lib/http/api", () => ({
Expand All @@ -16,9 +18,11 @@ vi.mock("@/lib/http/api", () => ({
}));
vi.mock("@/lib/gen2/superset", () => ({
createGen2SupersetEntry: mocks.create,
moveGen2SupersetEntry: mocks.move,
deleteGen2SupersetEntry: mocks.remove,
}));

import { POST } from "./route";
import { DELETE, PATCH, POST } from "./route";

const workspaceId = "e010bd2c-a3c1-438f-acef-166287a3b1cb";
const userId = "2f2387ed-4a63-4b05-88cc-266d65f7b82b";
Expand All @@ -33,24 +37,85 @@ function post(body: unknown) {
});
}

function request(method: "PATCH" | "DELETE", body: unknown) {
return new Request(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}

describe("Superset entry route", () => {
beforeEach(() => mocks.getApiUser.mockResolvedValue({ id: userId }));
afterEach(() => vi.resetAllMocks());

it("creates a root-level file through the authenticated bridge", async () => {
it("creates a file in the selected folder through the authenticated bridge", async () => {
mocks.create.mockResolvedValue({
path: "notes.md",
kind: "file",
size: 0,
});
const input = { worktreeId: "main", name: "notes.md", kind: "file" };
const input = {
worktreeId: "main",
parentPath: "notes",
name: "notes.md",
kind: "file",
};
const response = await POST(post(input), { params });
expect(mocks.create).toHaveBeenCalledWith(workspaceId, userId, input);
expect(await response.json()).toEqual({
entry: { path: "notes.md", kind: "file", size: 0 },
});
});

it("keeps root-level creation compatible when no parent path is sent", async () => {
mocks.create.mockResolvedValue({ path: "notes.md", kind: "file", size: 0 });

const response = await POST(
post({ worktreeId: "main", name: "notes.md", kind: "file" }),
{ params },
);

expect(mocks.create).toHaveBeenCalledWith(workspaceId, userId, {
worktreeId: "main",
parentPath: "",
name: "notes.md",
kind: "file",
});
expect(response.status).toBe(200);
});

it("renames an entry through the authenticated bridge", async () => {
const input = {
worktreeId: "main",
path: "notes/draft.md",
parentPath: "notes",
name: "published.md",
};
mocks.move.mockResolvedValue({
path: "notes/published.md",
kind: "file",
size: 12,
});

const response = await PATCH(request("PATCH", input), { params });

expect(mocks.move).toHaveBeenCalledWith(workspaceId, userId, input);
expect(await response.json()).toEqual({
entry: { path: "notes/published.md", kind: "file", size: 12 },
});
});

it("deletes an entry through the authenticated bridge", async () => {
const input = { worktreeId: "main", path: "notes/archive" };
mocks.remove.mockResolvedValue(input.path);

const response = await DELETE(request("DELETE", input), { params });

expect(mocks.remove).toHaveBeenCalledWith(workspaceId, userId, input);
expect(await response.json()).toEqual({ path: input.path });
});

it("rejects a path instead of a single entry name", async () => {
const response = await POST(
post({ worktreeId: "main", name: "src/notes.md", kind: "file" }),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { gen2SupersetCreateEntryRequestSchema } from "@codev/contracts";
import {
gen2SupersetCreateEntryRequestSchema,
gen2SupersetDeleteEntryRequestSchema,
gen2SupersetMoveEntryRequestSchema,
} from "@codev/contracts";

import { createGen2SupersetEntry } from "@/lib/gen2/superset";
import {
createGen2SupersetEntry,
deleteGen2SupersetEntry,
moveGen2SupersetEntry,
} from "@/lib/gen2/superset";
import { readJson, withUser } from "@/lib/http/api-route";

type Params = { workspaceId: string };
Expand All @@ -16,3 +24,23 @@ export const POST = withUser<Params>(
},
{ errorStatus: 502 },
);

export const PATCH = withUser<Params>(
async ({ request, user, params: { workspaceId } }) => {
const input = await readJson(request, gen2SupersetMoveEntryRequestSchema);
return Response.json({
entry: await moveGen2SupersetEntry(workspaceId, user.id, input),
});
},
{ errorStatus: 502 },
);

export const DELETE = withUser<Params>(
async ({ request, user, params: { workspaceId } }) => {
const input = await readJson(request, gen2SupersetDeleteEntryRequestSchema);
return Response.json({
path: await deleteGen2SupersetEntry(workspaceId, user.id, input),
});
},
{ errorStatus: 502 },
);
Loading
Loading