* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
267 lines
7.3 KiB
TypeScript
267 lines
7.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
|
import { migratePersistedState, type MigrateLegacyImages } from "./migration";
|
|
import { isAttachmentMetadata, type DraftRecord } from "./state";
|
|
|
|
const passThroughMigrateLegacyImages: MigrateLegacyImages = async (images) =>
|
|
images.filter(isAttachmentMetadata);
|
|
|
|
function activeDraft(
|
|
text: string,
|
|
updatedAt: number,
|
|
attachments: UserComposerAttachment[] = [],
|
|
): DraftRecord {
|
|
return {
|
|
input: { text, attachments },
|
|
lifecycle: "active",
|
|
updatedAt,
|
|
version: 1,
|
|
};
|
|
}
|
|
|
|
function githubIssueAttachment(
|
|
number: number,
|
|
): Extract<UserComposerAttachment, { kind: "github_issue" }> {
|
|
return {
|
|
kind: "github_issue",
|
|
item: {
|
|
kind: "issue",
|
|
number,
|
|
title: `Review item ${number}`,
|
|
url: `https://example.com/issues/${number}`,
|
|
state: "open",
|
|
body: null,
|
|
labels: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function githubPrAttachment(
|
|
number: number,
|
|
): Extract<UserComposerAttachment, { kind: "github_pr" }> {
|
|
return {
|
|
kind: "github_pr",
|
|
item: {
|
|
kind: "change_request",
|
|
number,
|
|
title: `Review item ${number}`,
|
|
url: `https://example.com/pulls/${number}`,
|
|
state: "open",
|
|
body: null,
|
|
labels: [],
|
|
baseRefName: "main",
|
|
headRefName: "feature/legacy",
|
|
},
|
|
};
|
|
}
|
|
|
|
function workspaceReviewAttachment(): Extract<ComposerAttachment, { kind: "review" }> {
|
|
return {
|
|
kind: "review",
|
|
reviewDraftKey: "review:key",
|
|
commentCount: 1,
|
|
attachment: {
|
|
type: "review",
|
|
mimeType: "application/paseo-review",
|
|
cwd: "/repo",
|
|
mode: "uncommitted",
|
|
baseRef: null,
|
|
comments: [
|
|
{
|
|
filePath: "src/example.ts",
|
|
side: "new",
|
|
lineNumber: 41,
|
|
body: "Please simplify this.",
|
|
context: {
|
|
hunkHeader: "@@ -40,1 +40,1 @@",
|
|
targetLine: {
|
|
oldLineNumber: null,
|
|
newLineNumber: 41,
|
|
type: "add",
|
|
content: "const value = newValue;",
|
|
},
|
|
lines: [
|
|
{
|
|
oldLineNumber: null,
|
|
newLineNumber: 41,
|
|
type: "add",
|
|
content: "const value = newValue;",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("draft-store migration", () => {
|
|
it("promotes the newest legacy New Workspace draft into the singleton surface", async () => {
|
|
const forkDraft = activeDraft("fork context", 1700000000003);
|
|
const agentDraft = activeDraft("agent prompt", 1700000000004);
|
|
|
|
const migrated = await migratePersistedState(
|
|
{
|
|
drafts: {
|
|
"new-workspace:server-a:/project/older": activeDraft(
|
|
"older new workspace prompt",
|
|
1700000000001,
|
|
),
|
|
"new-workspace:server-b:/project/newer": activeDraft(
|
|
"newer new workspace prompt",
|
|
1700000000002,
|
|
),
|
|
"new-workspace:draft:fork-1": forkDraft,
|
|
"agent:server-a:agent-1": agentDraft,
|
|
},
|
|
createModalDraft: null,
|
|
},
|
|
{ migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 1700000000005 },
|
|
);
|
|
|
|
expect(migrated.drafts).toEqual({
|
|
"new-workspace": activeDraft("newer new workspace prompt", 1700000000002),
|
|
"new-workspace:draft:fork-1": forkDraft,
|
|
"agent:server-a:agent-1": agentDraft,
|
|
});
|
|
});
|
|
|
|
it("drops unowned checkout PR context when promoting a scoped New Workspace draft", async () => {
|
|
const issue = githubIssueAttachment(101);
|
|
const migrated = await migratePersistedState(
|
|
{
|
|
drafts: {
|
|
"new-workspace:server-a:/project/a": activeDraft("keep the prompt", 2, [
|
|
issue,
|
|
githubPrAttachment(202),
|
|
]),
|
|
},
|
|
createModalDraft: null,
|
|
},
|
|
{ migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 3 },
|
|
);
|
|
|
|
expect(migrated.drafts["new-workspace"]?.input).toEqual({
|
|
text: "keep the prompt",
|
|
attachments: [issue],
|
|
});
|
|
});
|
|
|
|
it("normalizes legacy image metadata into image attachments and strips persisted preview URLs", async () => {
|
|
const migrated = await migratePersistedState(
|
|
{
|
|
drafts: {
|
|
"agent:server:agent": {
|
|
input: {
|
|
text: "hello",
|
|
images: [
|
|
{
|
|
id: "att-1",
|
|
mimeType: "image/png",
|
|
storageType: "desktop-file",
|
|
storageKey: "/tmp/att-1.png",
|
|
createdAt: 1700000000000,
|
|
previewUri: "asset://should-not-persist",
|
|
},
|
|
],
|
|
},
|
|
lifecycle: "active",
|
|
updatedAt: 1700000000001,
|
|
version: 1,
|
|
},
|
|
},
|
|
createModalDraft: null,
|
|
},
|
|
{ migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 1700000000002 },
|
|
);
|
|
|
|
expect(migrated.drafts["agent:server:agent"]?.input).toEqual({
|
|
text: "hello",
|
|
attachments: [
|
|
{
|
|
kind: "image",
|
|
metadata: {
|
|
id: "att-1",
|
|
mimeType: "image/png",
|
|
storageType: "desktop-file",
|
|
storageKey: "/tmp/att-1.png",
|
|
createdAt: 1700000000000,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("hydrates old persisted drafts that still include cwd", async () => {
|
|
const original = {
|
|
drafts: {
|
|
"agent:server:agent": {
|
|
input: {
|
|
text: "hello",
|
|
attachments: [
|
|
{
|
|
kind: "image",
|
|
metadata: {
|
|
id: "att-1",
|
|
mimeType: "image/jpeg",
|
|
storageType: "web-indexeddb",
|
|
storageKey: "att-1",
|
|
createdAt: 1700000000000,
|
|
},
|
|
},
|
|
],
|
|
cwd: "/repo",
|
|
},
|
|
lifecycle: "active",
|
|
updatedAt: 1700000000001,
|
|
version: 2,
|
|
},
|
|
},
|
|
createModalDraft: null,
|
|
};
|
|
|
|
const ports = { migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 1700000000002 };
|
|
const once = await migratePersistedState(original, ports);
|
|
const twice = await migratePersistedState(once, ports);
|
|
|
|
expect(twice).toEqual(once);
|
|
expect(twice.drafts["agent:server:agent"]?.input).toEqual({
|
|
text: "hello",
|
|
attachments: [
|
|
{
|
|
kind: "image",
|
|
metadata: {
|
|
id: "att-1",
|
|
mimeType: "image/jpeg",
|
|
storageType: "web-indexeddb",
|
|
storageKey: "att-1",
|
|
createdAt: 1700000000000,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("rejects workspace review attachments from migrated draft attachments", async () => {
|
|
const migrated = await migratePersistedState(
|
|
{
|
|
drafts: {
|
|
"agent:server:agent": {
|
|
input: {
|
|
text: "hello",
|
|
attachments: [workspaceReviewAttachment()],
|
|
},
|
|
lifecycle: "active",
|
|
updatedAt: 1700000000001,
|
|
version: 2,
|
|
},
|
|
},
|
|
createModalDraft: null,
|
|
},
|
|
{ migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 1700000000002 },
|
|
);
|
|
|
|
expect(migrated.drafts["agent:server:agent"]?.input.attachments).toEqual([]);
|
|
});
|
|
});
|