Compare commits
3 Commits
065b1322bb
...
efe30df03a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efe30df03a | ||
|
|
cb3d65be2a | ||
|
|
a15da068d7 |
@ -1,13 +1,14 @@
|
|||||||
// Onboarding scan — real filesystem scan, Git analysis, and profile compilation.
|
// Onboarding scan — real filesystem scan, Git analysis, and profile compilation.
|
||||||
// Runs synchronously on the host machine (no sandbox needed).
|
// Runs synchronously on the host machine (no sandbox needed).
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { access, readdir, realpath, stat } from "node:fs/promises";
|
import { access, readdir, stat } from "node:fs/promises";
|
||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
import type { Actor } from "@rakazo/contracts";
|
import type { Actor } from "@rakazo/contracts";
|
||||||
import type { PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
import {
|
import {
|
||||||
createSpaceForMember,
|
createSpaceForMember,
|
||||||
InvalidSpaceNameError,
|
InvalidSpaceNameError,
|
||||||
|
resolveProjectWorkspaceDirectory,
|
||||||
SpaceLimitError,
|
SpaceLimitError,
|
||||||
wireProjectWorkspace,
|
wireProjectWorkspace,
|
||||||
} from "@rakazo/db";
|
} from "@rakazo/db";
|
||||||
@ -656,7 +657,8 @@ async function createProjects(
|
|||||||
|
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
// Compare and link the real folder, so the next scan recognizes the projects of this one.
|
// Compare and link the real folder, so the next scan recognizes the projects of this one.
|
||||||
const folder = await realpath(repo.path).catch(() => null);
|
// Checking it before the project exists means a folder that cannot be linked leaves none.
|
||||||
|
const folder = await resolveProjectWorkspaceDirectory(repo.path).catch(() => null);
|
||||||
if (!folder || linkedFolders.has(folder)) continue;
|
if (!folder || linkedFolders.has(folder)) continue;
|
||||||
let space: { id: string; name: string };
|
let space: { id: string; name: string };
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -137,6 +137,7 @@ import {
|
|||||||
releaseSpaceDeletionClaim,
|
releaseSpaceDeletionClaim,
|
||||||
renewSpaceDeletionClaim,
|
renewSpaceDeletionClaim,
|
||||||
requireMembership,
|
requireMembership,
|
||||||
|
resolveProjectWorkspaceDirectory,
|
||||||
SPACE_DELETION_CLAIM_TIMEOUT_MS,
|
SPACE_DELETION_CLAIM_TIMEOUT_MS,
|
||||||
SpaceDeletionInProgressError,
|
SpaceDeletionInProgressError,
|
||||||
SpaceLimitError,
|
SpaceLimitError,
|
||||||
@ -573,6 +574,18 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
if (input.workspacePath && !context.actor.isDeploymentOwner) {
|
if (input.workspacePath && !context.actor.isDeploymentOwner) {
|
||||||
throw new ORPCError("FORBIDDEN");
|
throw new ORPCError("FORBIDDEN");
|
||||||
}
|
}
|
||||||
|
// Check the folder before the space exists, so a bad path leaves no space behind.
|
||||||
|
let workspacePath: string | null = null;
|
||||||
|
if (input.workspacePath) {
|
||||||
|
try {
|
||||||
|
workspacePath = await resolveProjectWorkspaceDirectory(input.workspacePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof InvalidProjectWorkspacePathError) {
|
||||||
|
throw new ORPCError("BAD_REQUEST", { message: error.message });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
let space: { id: string; name: string };
|
let space: { id: string; name: string };
|
||||||
try {
|
try {
|
||||||
space = await createSpaceForMember(deps.prisma, {
|
space = await createSpaceForMember(deps.prisma, {
|
||||||
@ -586,20 +599,9 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
let workspace = null;
|
const workspace = workspacePath
|
||||||
if (input.workspacePath) {
|
? await wireProjectWorkspace(deps.prisma, { spaceId: space.id, workspacePath })
|
||||||
try {
|
: null;
|
||||||
workspace = await wireProjectWorkspace(deps.prisma, {
|
|
||||||
spaceId: space.id,
|
|
||||||
workspacePath: input.workspacePath,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof InvalidProjectWorkspacePathError) {
|
|
||||||
throw new ORPCError("BAD_REQUEST", { message: error.message });
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id: space.id,
|
id: space.id,
|
||||||
name: space.name,
|
name: space.name,
|
||||||
|
|||||||
@ -69,9 +69,10 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
|||||||
})
|
})
|
||||||
.toBe("waiting_takeover");
|
.toBe("waiting_takeover");
|
||||||
await captureScreenshot(page, testInfo, "08-protected-input-request");
|
await captureScreenshot(page, testInfo, "08-protected-input-request");
|
||||||
await page.getByTitle("Agent computer").click();
|
// A takeover request brings the bot's screen beside the chat on its own.
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidePanel).toHaveCSS("width", "384px");
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
|
await expect(sidePanel).toHaveAttribute("data-auto", "");
|
||||||
const [mainBox, panelBox] = await Promise.all([
|
const [mainBox, panelBox] = await Promise.all([
|
||||||
page.locator("main").boundingBox(),
|
page.locator("main").boundingBox(),
|
||||||
sidePanel.boundingBox(),
|
sidePanel.boundingBox(),
|
||||||
|
|||||||
@ -303,9 +303,11 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
|||||||
const groupMarkdown = page.getByRole("button", { name: "Preview group-preview.md" });
|
const groupMarkdown = page.getByRole("button", { name: "Preview group-preview.md" });
|
||||||
await expect(groupMarkdown).toBeVisible({ timeout: 30_000 });
|
await expect(groupMarkdown).toBeVisible({ timeout: 30_000 });
|
||||||
await groupMarkdown.click();
|
await groupMarkdown.click();
|
||||||
const markdownDialog = page.getByRole("dialog", { name: "group-preview.md" });
|
// Wide windows open the file beside the group conversation.
|
||||||
await expect(markdownDialog.getByRole("heading", { name: "Group artifact" })).toBeVisible();
|
const markdownPane = page.getByTestId("artifact-pane");
|
||||||
await markdownDialog.getByRole("button", { name: "Close preview" }).click();
|
await expect(markdownPane.getByRole("heading", { name: "Group artifact" })).toBeVisible();
|
||||||
|
await markdownPane.getByRole("button", { name: "Close panel" }).click();
|
||||||
|
await expect(markdownPane).toHaveCount(0);
|
||||||
|
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await expect(page.getByRole("button", { name: "Open navigation" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Open navigation" })).toBeVisible();
|
||||||
|
|||||||
@ -3370,7 +3370,9 @@ export function ShellPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const embeddedScreenUrl = embeddableScreenUrl(screenUrl);
|
const embeddedScreenUrl = embeddableScreenUrl(screenUrl);
|
||||||
const workspacePaneOpen = panel === "computer" || panel === "artifact";
|
// Only what the work opened gets the wide pane; settings, routines and a hand-opened screen keep
|
||||||
|
// one width so switching between them never jumps.
|
||||||
|
const workspacePaneOpen = panel === "artifact" || (panel === "computer" && autoPanel === panel);
|
||||||
const hasControl = userHoldsComputerControl(computer, active?.id);
|
const hasControl = userHoldsComputerControl(computer, active?.id);
|
||||||
const hideScreenLoadError = computerErrorFromScreen && Boolean(embeddedScreenUrl);
|
const hideScreenLoadError = computerErrorFromScreen && Boolean(embeddedScreenUrl);
|
||||||
const computerScreenError =
|
const computerScreenError =
|
||||||
|
|||||||
@ -28,6 +28,7 @@ function snapshot(overrides: Partial<WorkspaceFocusSnapshot> = {}): WorkspaceFoc
|
|||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
id: "msg-report",
|
id: "msg-report",
|
||||||
|
role: "bot",
|
||||||
runId: "run-1",
|
runId: "run-1",
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
@ -73,7 +74,7 @@ describe("deriveWorkspaceFocus", () => {
|
|||||||
for (const computer of [
|
for (const computer of [
|
||||||
{ ...runningComputer, kind: "desktop" },
|
{ ...runningComputer, kind: "desktop" },
|
||||||
{ ...runningComputer, kind: "none" },
|
{ ...runningComputer, kind: "none" },
|
||||||
{ ...runningComputer, screenAvailable: false },
|
null,
|
||||||
]) {
|
]) {
|
||||||
expect(
|
expect(
|
||||||
deriveWorkspaceFocus(snapshot({ computer }), { screenRunId: "run-1" }).pane,
|
deriveWorkspaceFocus(snapshot({ computer }), { screenRunId: "run-1" }).pane,
|
||||||
@ -81,6 +82,25 @@ describe("deriveWorkspaceFocus", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("trusts screen work over a computer status that has not caught up with the boot", () => {
|
||||||
|
const stale = { ...runningComputer, state: "stopped", screenAvailable: false };
|
||||||
|
expect(deriveWorkspaceFocus(snapshot({ computer: stale }), { screenRunId: "run-1" }).pane).toBe(
|
||||||
|
"screen",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
deriveWorkspaceFocus(
|
||||||
|
snapshot({
|
||||||
|
run: { id: "run-1", status: "waiting_takeover" },
|
||||||
|
computer: { ...stale, takeoverRequested: true },
|
||||||
|
}),
|
||||||
|
).pane,
|
||||||
|
).toBe("screen");
|
||||||
|
// Without evidence of screen work, a learned default never boots a stopped computer.
|
||||||
|
expect(
|
||||||
|
deriveWorkspaceFocus(snapshot({ computer: stale }), { screenMode: "always" }).pane,
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows the screen for a takeover request even when the user turned auto off", () => {
|
it("shows the screen for a takeover request even when the user turned auto off", () => {
|
||||||
const focus = deriveWorkspaceFocus(
|
const focus = deriveWorkspaceFocus(
|
||||||
snapshot({
|
snapshot({
|
||||||
@ -104,7 +124,7 @@ describe("deriveWorkspaceFocus", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a file the latest run presented", () => {
|
it("shows the file the bot presented as its latest result", () => {
|
||||||
const focus = deriveWorkspaceFocus(
|
const focus = deriveWorkspaceFocus(
|
||||||
snapshot({ run: { id: "run-1", status: "completed" }, messages: [report] }),
|
snapshot({ run: { id: "run-1", status: "completed" }, messages: [report] }),
|
||||||
);
|
);
|
||||||
@ -118,17 +138,20 @@ describe("deriveWorkspaceFocus", () => {
|
|||||||
expect(focus.key).toBe("artifact:art-1|ask:none");
|
expect(focus.key).toBe("artifact:art-1|ask:none");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores files that were only attached, presented by older runs, or not previewable", () => {
|
it("ignores files that were only attached, presented before the user wrote again, or not previewable", () => {
|
||||||
const attached = {
|
const attached = {
|
||||||
...report,
|
...report,
|
||||||
blocks: [{ ...report.blocks[0]!, present: undefined }],
|
blocks: [{ ...report.blocks[0]!, present: undefined }],
|
||||||
};
|
};
|
||||||
expect(deriveWorkspaceFocus(snapshot({ messages: [attached] })).pane).toBeNull();
|
expect(deriveWorkspaceFocus(snapshot({ messages: [attached] })).pane).toBeNull();
|
||||||
|
const followUp = { id: "msg-user", role: "user", runId: "run-2", blocks: [{ kind: "text" }] };
|
||||||
|
expect(deriveWorkspaceFocus(snapshot({ messages: [report, followUp] })).pane).toBeNull();
|
||||||
|
// Another member's newer run in a group does not hide the result.
|
||||||
expect(
|
expect(
|
||||||
deriveWorkspaceFocus(
|
deriveWorkspaceFocus(
|
||||||
snapshot({ run: { id: "run-2", status: "running" }, messages: [report] }),
|
snapshot({ run: { id: "run-9", status: "running" }, messages: [report] }),
|
||||||
).pane,
|
).pane,
|
||||||
).toBeNull();
|
).toBe("artifact");
|
||||||
const pdf = {
|
const pdf = {
|
||||||
...report,
|
...report,
|
||||||
blocks: [{ ...report.blocks[0]!, mimeType: "application/pdf", name: "report.pdf" }],
|
blocks: [{ ...report.blocks[0]!, mimeType: "application/pdf", name: "report.pdf" }],
|
||||||
@ -181,7 +204,10 @@ describe("applyWorkspaceFocus", () => {
|
|||||||
|
|
||||||
it("keeps a presented file open while the user follows up", () => {
|
it("keeps a presented file open while the user follows up", () => {
|
||||||
const followUp = deriveWorkspaceFocus(
|
const followUp = deriveWorkspaceFocus(
|
||||||
snapshot({ run: { id: "run-2", status: "running" }, messages: [report] }),
|
snapshot({
|
||||||
|
run: { id: "run-2", status: "running" },
|
||||||
|
messages: [report, { id: "msg-user", role: "user", blocks: [{ kind: "text" }] }],
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(followUp.pane).toBeNull();
|
expect(followUp.pane).toBeNull();
|
||||||
expect(applyWorkspaceFocus(artifact, followUp, { open: "artifact", auto: true })).toBeNull();
|
expect(applyWorkspaceFocus(artifact, followUp, { open: "artifact", auto: true })).toBeNull();
|
||||||
|
|||||||
@ -37,7 +37,12 @@ type FocusBlock = {
|
|||||||
type FocusRun = { id: string; status: string };
|
type FocusRun = { id: string; status: string };
|
||||||
|
|
||||||
export type WorkspaceFocusSnapshot = {
|
export type WorkspaceFocusSnapshot = {
|
||||||
messages: readonly { id: string; runId?: string | null; blocks: readonly FocusBlock[] }[];
|
messages: readonly {
|
||||||
|
id: string;
|
||||||
|
role?: string;
|
||||||
|
runId?: string | null;
|
||||||
|
blocks: readonly FocusBlock[];
|
||||||
|
}[];
|
||||||
run?: FocusRun | null;
|
run?: FocusRun | null;
|
||||||
activeRuns?: readonly FocusRun[];
|
activeRuns?: readonly FocusRun[];
|
||||||
computer?: {
|
computer?: {
|
||||||
@ -89,12 +94,12 @@ function workingRunIds(snapshot: WorkspaceFocusSnapshot): Set<string> {
|
|||||||
return new Set(runs.filter((run) => WORKING_RUN_STATUSES.has(run.status)).map((run) => run.id));
|
return new Set(runs.filter((run) => WORKING_RUN_STATUSES.has(run.status)).map((run) => run.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The newest file a bot presented since the user last wrote; a new request starts fresh. */
|
||||||
function presentedArtifact(snapshot: WorkspaceFocusSnapshot): WorkspaceArtifact | null {
|
function presentedArtifact(snapshot: WorkspaceFocusSnapshot): WorkspaceArtifact | null {
|
||||||
const latestRunId = snapshot.run?.id;
|
|
||||||
if (!latestRunId) return null;
|
|
||||||
for (let index = snapshot.messages.length - 1; index >= 0; index -= 1) {
|
for (let index = snapshot.messages.length - 1; index >= 0; index -= 1) {
|
||||||
const message = snapshot.messages[index];
|
const message = snapshot.messages[index];
|
||||||
if (!message || message.runId !== latestRunId) continue;
|
if (!message) continue;
|
||||||
|
if (message.role === "user") return null;
|
||||||
for (let blockIndex = message.blocks.length - 1; blockIndex >= 0; blockIndex -= 1) {
|
for (let blockIndex = message.blocks.length - 1; blockIndex >= 0; blockIndex -= 1) {
|
||||||
const block = message.blocks[blockIndex];
|
const block = message.blocks[blockIndex];
|
||||||
if (
|
if (
|
||||||
@ -119,7 +124,7 @@ function presentedArtifact(snapshot: WorkspaceFocusSnapshot): WorkspaceArtifact
|
|||||||
/**
|
/**
|
||||||
* Derives the pane a conversation should show from what the bot is actually doing: a
|
* Derives the pane a conversation should show from what the bot is actually doing: a
|
||||||
* takeover request or live work on its screen shows the screen, a file the bot presented
|
* takeover request or live work on its screen shows the screen, a file the bot presented
|
||||||
* as its result shows that file, and everything else stays a plain conversation.
|
* as its latest result shows that file, and everything else stays a plain conversation.
|
||||||
*/
|
*/
|
||||||
export function deriveWorkspaceFocus(
|
export function deriveWorkspaceFocus(
|
||||||
snapshot: WorkspaceFocusSnapshot | null,
|
snapshot: WorkspaceFocusSnapshot | null,
|
||||||
@ -134,16 +139,19 @@ export function deriveWorkspaceFocus(
|
|||||||
activeRuns: snapshot.activeRuns,
|
activeRuns: snapshot.activeRuns,
|
||||||
});
|
});
|
||||||
const computer = snapshot.computer;
|
const computer = snapshot.computer;
|
||||||
const screenViewable =
|
|
||||||
Boolean(computer?.screenAvailable) && computer?.kind !== "desktop" && computer?.kind !== "none";
|
|
||||||
const takeover = Boolean(computer?.takeoverRequested) && screenViewable;
|
|
||||||
const working = workingRunIds(snapshot);
|
const working = workingRunIds(snapshot);
|
||||||
const mode = live.screenMode ?? "auto";
|
const mode = live.screenMode ?? "auto";
|
||||||
const pointerWork = live.screenRunId != null && working.has(live.screenRunId);
|
const pointerWork =
|
||||||
|
Boolean(live.screenActive) || (live.screenRunId != null && working.has(live.screenRunId));
|
||||||
|
// A desktop host or a bot without a computer has no screen to embed.
|
||||||
|
const embeddable = Boolean(computer) && computer?.kind !== "desktop" && computer?.kind !== "none";
|
||||||
|
// A bot that points at its screen or asks for a takeover has a running screen, even when the
|
||||||
|
// client's computer status has not caught up with a boot that happened inside the run.
|
||||||
|
const takeover = embeddable && Boolean(computer?.takeoverRequested);
|
||||||
const screenWork =
|
const screenWork =
|
||||||
screenViewable &&
|
embeddable &&
|
||||||
mode !== "never" &&
|
mode !== "never" &&
|
||||||
((working.size > 0 && (mode === "always" || pointerWork)) || Boolean(live.screenActive));
|
(pointerWork || (mode === "always" && working.size > 0 && Boolean(computer?.screenAvailable)));
|
||||||
const artifact = presentedArtifact(snapshot);
|
const artifact = presentedArtifact(snapshot);
|
||||||
const pane: WorkspacePane | null =
|
const pane: WorkspacePane | null =
|
||||||
takeover || screenWork ? "screen" : artifact ? "artifact" : null;
|
takeover || screenWork ? "screen" : artifact ? "artifact" : null;
|
||||||
|
|||||||
@ -72,7 +72,7 @@ describe("project workspace wiring", () => {
|
|||||||
|
|
||||||
const wired = await wireProjectWorkspace(prisma as unknown as Pick<PrismaClient, "space">, {
|
const wired = await wireProjectWorkspace(prisma as unknown as Pick<PrismaClient, "space">, {
|
||||||
spaceId: "space-1",
|
spaceId: "space-1",
|
||||||
workspacePath: root,
|
workspacePath: await resolveProjectWorkspaceDirectory(root),
|
||||||
});
|
});
|
||||||
expect(wired.path).toBe(realpathSync(root));
|
expect(wired.path).toBe(realpathSync(root));
|
||||||
expect(wired.git).toEqual({
|
expect(wired.git).toEqual({
|
||||||
|
|||||||
@ -50,18 +50,17 @@ export interface WiredProjectWorkspace {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persist a host folder and optional git metadata on a newly created space. */
|
/** Persist a folder from `resolveProjectWorkspaceDirectory`, and its git metadata, on a new space. */
|
||||||
export async function wireProjectWorkspace(
|
export async function wireProjectWorkspace(
|
||||||
prisma: Pick<PrismaClient, "space">,
|
prisma: Pick<PrismaClient, "space">,
|
||||||
input: { spaceId: string; workspacePath: string },
|
input: { spaceId: string; workspacePath: string },
|
||||||
): Promise<WiredProjectWorkspace> {
|
): Promise<WiredProjectWorkspace> {
|
||||||
const resolved = await resolveProjectWorkspaceDirectory(input.workspacePath);
|
const git = await readGitCheckout(input.workspacePath);
|
||||||
const git = await readGitCheckout(resolved);
|
|
||||||
|
|
||||||
await prisma.space.update({
|
await prisma.space.update({
|
||||||
where: { id: input.spaceId },
|
where: { id: input.spaceId },
|
||||||
data: {
|
data: {
|
||||||
workspacePath: resolved,
|
workspacePath: input.workspacePath,
|
||||||
gitRemoteUrl: git.present && git.remoteUrl ? git.remoteUrl : null,
|
gitRemoteUrl: git.present && git.remoteUrl ? git.remoteUrl : null,
|
||||||
gitDefaultBranch: git.present && git.defaultBranch ? git.defaultBranch : null,
|
gitDefaultBranch: git.present && git.defaultBranch ? git.defaultBranch : null,
|
||||||
gitHeadCommit: git.present ? git.headCommit : null,
|
gitHeadCommit: git.present ? git.headCommit : null,
|
||||||
@ -69,7 +68,7 @@ export async function wireProjectWorkspace(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: resolved,
|
path: input.workspacePath,
|
||||||
git:
|
git:
|
||||||
git.present && git.remoteUrl && git.defaultBranch
|
git.present && git.remoteUrl && git.defaultBranch
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -2239,15 +2239,25 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
const legacyNavigation = await rpc<SpaceNavigation>(app, member, "spaces/list");
|
const legacyNavigation = await rpc<SpaceNavigation>(app, member, "spaces/list");
|
||||||
expect(legacyNavigation.spaces.find((space) => space.id === legacy.id)?.workspace).toBeNull();
|
expect(legacyNavigation.spaces.find((space) => space.id === legacy.id)?.workspace).toBeNull();
|
||||||
|
|
||||||
|
// A bad folder is rejected before the project exists, so fixing it and retrying leaves one.
|
||||||
|
for (const badPath of [path.join(hostFolder, "missing"), path.join(hostFolder, ".env")]) {
|
||||||
|
const rejected = await raw(app, owner, "spaces/create", {
|
||||||
|
name: "Owner folder",
|
||||||
|
workspacePath: badPath,
|
||||||
|
});
|
||||||
|
expect(rejected.status, badPath).toBe(400);
|
||||||
|
}
|
||||||
const linked = await rpc<Space>(app, owner, "spaces/create", {
|
const linked = await rpc<Space>(app, owner, "spaces/create", {
|
||||||
name: "Owner folder",
|
name: "Owner folder",
|
||||||
workspacePath: hostFolder,
|
workspacePath: hostFolder,
|
||||||
});
|
});
|
||||||
expect(linked.workspace?.path).toBe(hostFolder);
|
expect(linked.workspace?.path).toBe(hostFolder);
|
||||||
const ownerNavigation = await rpc<SpaceNavigation>(app, owner, "spaces/list");
|
const ownerNavigation = await rpc<SpaceNavigation>(app, owner, "spaces/list");
|
||||||
expect(ownerNavigation.spaces.find((space) => space.id === linked.id)?.workspace?.path).toBe(
|
expect(
|
||||||
hostFolder,
|
ownerNavigation.spaces
|
||||||
);
|
.filter((space) => space.name === "Owner folder")
|
||||||
|
.map((space) => space.workspace?.path),
|
||||||
|
).toEqual([hostFolder]);
|
||||||
const ownerBot = await rpc<Bot>(
|
const ownerBot = await rpc<Bot>(
|
||||||
app,
|
app,
|
||||||
owner,
|
owner,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user