@fungi.computer/bonsai 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/backend/background.ts +259 -0
- package/backend/change-copy.ts +99 -0
- package/backend/changes.ts +530 -0
- package/backend/diff.ts +228 -0
- package/backend/file-edits.ts +850 -0
- package/backend/files-copy.ts +361 -0
- package/backend/gills.ts +125 -0
- package/backend/git.ts +123 -0
- package/backend/graph-commit-copies.ts +321 -0
- package/backend/history.ts +178 -0
- package/backend/index.ts +144 -0
- package/backend/insights.ts +256 -0
- package/backend/merge.ts +999 -0
- package/backend/methods.ts +835 -0
- package/backend/policy.ts +160 -0
- package/backend/read-copy-page.ts +77 -0
- package/backend/repository-copy.ts +70 -0
- package/backend/reviews.ts +317 -0
- package/backend/service.ts +892 -0
- package/backend/state.ts +914 -0
- package/demo/koi-pond/README.md +17 -0
- package/demo/koi-pond/index.html +11 -0
- package/demo/koi-pond/package.json +13 -0
- package/demo/koi-pond/pnpm-lock.yaml +450 -0
- package/demo/koi-pond/src/fish.ts +32 -0
- package/demo/koi-pond/src/main.ts +24 -0
- package/demo/koi-pond/src/pond.ts +27 -0
- package/docs/media/bonsai-insights.png +0 -0
- package/docs/media/overview-phone.png +0 -0
- package/docs/media/overview.png +0 -0
- package/package.json +152 -3
- package/scripts/agent.sh +55 -0
- package/scripts/demo.sh +233 -0
- package/scripts/rebuild-copies.mjs +34 -0
- package/scripts/release.mjs +52 -0
- package/scripts/runtime-proof.mjs +299 -0
- package/scripts/seed-demo.mjs +724 -0
- package/scripts/seed-step.mjs +37 -0
- package/scripts/shed-terminal-proof.mjs +774 -0
- package/shared/contract.ts +404 -0
- package/shared/hooks.ts +208 -0
- package/shared/insights.ts +90 -0
- package/shared/picker-options.ts +23 -0
- package/shared/presentation.ts +21 -0
- package/shared/repository-deletion.ts +4 -0
- package/test/apps-build-install.test.ts +302 -0
- package/test/background-runtime.test.ts +206 -0
- package/test/browser/board.browser.mjs +52 -0
- package/test/browser/capture.browser.mjs +1264 -0
- package/test/browser/edit-in-inkcap.browser.mjs +83 -0
- package/test/browser/repository-settings-fixture.tsx +108 -0
- package/test/browser/repository-settings.browser.mjs +136 -0
- package/test/browser/repository-settings.html +10 -0
- package/test/browser/visual-fixture.tsx +667 -0
- package/test/browser/visual.html +10 -0
- package/test/browser/vite.config.mjs +9 -0
- package/test/change-page.test.ts +916 -0
- package/test/changes.test.ts +1063 -0
- package/test/checks-workflow-template.test.mjs +234 -0
- package/test/diff.test.ts +70 -0
- package/test/edit-in-inkcap.test.ts +227 -0
- package/test/file-edits.test.ts +2714 -0
- package/test/files-read-copy-calls.test.ts +374 -0
- package/test/fork-adoption.test.ts +111 -0
- package/test/gills.test.ts +125 -0
- package/test/git-binding.fixture.ts +30 -0
- package/test/git-push.config.jsonc +9 -0
- package/test/git-push.native.mjs +413 -0
- package/test/git-push.worker.ts +280 -0
- package/test/graph-commit-read-copy-calls.test.ts +401 -0
- package/test/home-page.test.ts +170 -0
- package/test/hooks.test.ts +250 -0
- package/test/insight-data.test.ts +51 -0
- package/test/insight-layout.test.mjs +230 -0
- package/test/insights.test.ts +154 -0
- package/test/merge-background.test.ts +756 -0
- package/test/merge-conflict.test.ts +337 -0
- package/test/merge-declaration.test.ts +46 -0
- package/test/merge-repro.test.ts +561 -0
- package/test/merge-template-build.test.mjs +234 -0
- package/test/network-layout.test.ts +200 -0
- package/test/outbox.test.ts +424 -0
- package/test/parts.test.ts +86 -0
- package/test/pkt-line.test.ts +54 -0
- package/test/policy.test.ts +172 -0
- package/test/read-copy-calls.test.ts +691 -0
- package/test/read-copy-composer.test.ts +388 -0
- package/test/read-copy-lifetimes.test.ts +214 -0
- package/test/read-copy-rpc.config.jsonc +12 -0
- package/test/read-copy-rpc.native.mjs +47 -0
- package/test/read-copy-rpc.worker.ts +92 -0
- package/test/recent-activity.test.ts +62 -0
- package/test/repository-deletion.test.ts +500 -0
- package/test/seed-resume.test.mjs +91 -0
- package/test/storyboard-save-https-git.fixture.mjs +119 -0
- package/test/storyboard-save-template.test.mjs +545 -0
- package/test/ui-errors.test.ts +976 -0
- package/test/workflow-store.test.ts +115 -0
- package/tsconfig.json +36 -0
- package/turbo.jsonc +25 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +5 -0
- package/web/api.ts +224 -0
- package/web/app-build-panel.tsx +171 -0
- package/web/board.tsx +259 -0
- package/web/bonsai.css +642 -0
- package/web/change-page.tsx +715 -0
- package/web/change-rows.tsx +135 -0
- package/web/code-tab.tsx +267 -0
- package/web/commit-tree.tsx +152 -0
- package/web/format.ts +54 -0
- package/web/home-page.tsx +274 -0
- package/web/index.html +17 -0
- package/web/insight-data.ts +76 -0
- package/web/insight-tiles.tsx +534 -0
- package/web/insights.tsx +250 -0
- package/web/issues-panel.tsx +305 -0
- package/web/main.tsx +72 -0
- package/web/network-graph.tsx +647 -0
- package/web/overview.tsx +339 -0
- package/web/pages.tsx +469 -0
- package/web/parts.tsx +381 -0
- package/web/public/favicon.svg +1 -0
- package/web/repo-navigation.tsx +256 -0
- package/web/repo-settings.tsx +145 -0
- package/web/router.tsx +151 -0
- package/web/transport.ts +91 -0
- package/web/views.tsx +151 -0
- package/web/working-now.tsx +98 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The Code tab's Inkcap entry and unsynced chip at both widths and themes.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import console from "node:console";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { chromium } from "playwright";
|
|
7
|
+
|
|
8
|
+
const origin = process.argv[2];
|
|
9
|
+
const output = process.argv[3];
|
|
10
|
+
assert.ok(origin && output, "Usage: edit-in-inkcap.browser.mjs <origin> <dir>");
|
|
11
|
+
await mkdir(output, { recursive: true });
|
|
12
|
+
const browser = await chromium.launch({ headless: true });
|
|
13
|
+
try {
|
|
14
|
+
for (const scenario of ["code", "pending"])
|
|
15
|
+
for (const width of [1440, 390])
|
|
16
|
+
for (const theme of ["light", "dark"]) {
|
|
17
|
+
const page = await browser.newPage({
|
|
18
|
+
viewport: { width, height: width === 390 ? 844 : 900 },
|
|
19
|
+
colorScheme: theme,
|
|
20
|
+
reducedMotion: "reduce",
|
|
21
|
+
});
|
|
22
|
+
const errors = [];
|
|
23
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
24
|
+
await page.goto(
|
|
25
|
+
`${origin}/test/browser/visual.html?tab=code${scenario === "pending" ? "&pending" : ""}`,
|
|
26
|
+
);
|
|
27
|
+
await page.evaluate((value) => {
|
|
28
|
+
globalThis.document.documentElement.setAttribute("data-theme", value);
|
|
29
|
+
}, theme);
|
|
30
|
+
const edit = page.getByRole("button", {
|
|
31
|
+
name: "Edit in Inkcap",
|
|
32
|
+
exact: true,
|
|
33
|
+
});
|
|
34
|
+
await edit.waitFor();
|
|
35
|
+
if (scenario === "pending") {
|
|
36
|
+
await page.getByText("Unsynced edit", { exact: true }).waitFor();
|
|
37
|
+
await page
|
|
38
|
+
.getByRole("button", { name: "Sync", exact: true })
|
|
39
|
+
.waitFor();
|
|
40
|
+
await page
|
|
41
|
+
.getByText("Three new lilies float by the reeds.")
|
|
42
|
+
.waitFor();
|
|
43
|
+
}
|
|
44
|
+
await page.evaluate(() => globalThis.document.fonts.ready);
|
|
45
|
+
assert.deepEqual(errors, []);
|
|
46
|
+
await page.screenshot({
|
|
47
|
+
path: `${output}/${scenario}-${String(width)}-${theme}.png`,
|
|
48
|
+
fullPage: true,
|
|
49
|
+
});
|
|
50
|
+
await edit.click();
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
JSON.parse(
|
|
53
|
+
await page.evaluate(
|
|
54
|
+
() => globalThis.document.documentElement.dataset.openApp,
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
{
|
|
58
|
+
role: "files",
|
|
59
|
+
intent: {
|
|
60
|
+
kind: "navigate",
|
|
61
|
+
params: [
|
|
62
|
+
{ key: "repository", value: "koi-pond" },
|
|
63
|
+
{ key: "branch", value: "main" },
|
|
64
|
+
{ key: "path", value: "README.md" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
assert.ok(
|
|
70
|
+
(await page.evaluate(
|
|
71
|
+
() => globalThis.document.documentElement.scrollWidth,
|
|
72
|
+
)) <= width,
|
|
73
|
+
"no horizontal page scroll",
|
|
74
|
+
);
|
|
75
|
+
assert.deepEqual(errors, []);
|
|
76
|
+
await page.close();
|
|
77
|
+
}
|
|
78
|
+
console.log(
|
|
79
|
+
"PASS Edit in Inkcap entry and unsynced chip 1440/390 light/dark",
|
|
80
|
+
);
|
|
81
|
+
} finally {
|
|
82
|
+
await browser.close();
|
|
83
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Real repository pages with controlled App-method replies, no provider writes. */
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
import { createCapabilityRegistry } from "@fungi.computer/agar";
|
|
6
|
+
import { appMethods } from "@fungi.computer/agar/backend";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { repoDetail } from "../../shared/contract.js";
|
|
9
|
+
import { connect } from "../../web/transport.js";
|
|
10
|
+
import { RouterProvider, useRouter } from "../../web/router.js";
|
|
11
|
+
import { RepoPage } from "../../web/pages.js";
|
|
12
|
+
import "@fungi.computer/caps/fonts.css";
|
|
13
|
+
import "../../web/bonsai.css";
|
|
14
|
+
|
|
15
|
+
const name = "disposable-merge-1791840651624";
|
|
16
|
+
const live = { connected: true, error: null, fresh: new Set<number>() };
|
|
17
|
+
const detail = repoDetail.parse({
|
|
18
|
+
repo: {
|
|
19
|
+
name,
|
|
20
|
+
description: "Contest probe repository",
|
|
21
|
+
parent: null,
|
|
22
|
+
owner: null,
|
|
23
|
+
createdAt: 0,
|
|
24
|
+
lastPushAt: null,
|
|
25
|
+
forks: [],
|
|
26
|
+
openChanges: 0,
|
|
27
|
+
activity: [],
|
|
28
|
+
},
|
|
29
|
+
defaultBranch: "main",
|
|
30
|
+
refs: [],
|
|
31
|
+
cloneUrl: `https://example.test/git/${name}.git`,
|
|
32
|
+
empty: true,
|
|
33
|
+
});
|
|
34
|
+
const client = new QueryClient({
|
|
35
|
+
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
|
|
36
|
+
});
|
|
37
|
+
client.setQueryData(["repo", name], detail);
|
|
38
|
+
client.setQueryData(["events", name], { events: [] });
|
|
39
|
+
client.setQueryData(["changes", name], { changes: [] });
|
|
40
|
+
client.setQueryData(["agents"], { agents: [] });
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
let withdrawn = false;
|
|
43
|
+
const capabilities = createCapabilityRegistry();
|
|
44
|
+
capabilities.provide(appMethods, {
|
|
45
|
+
call: (method, inputJson) => {
|
|
46
|
+
if (method === "repos.delete") {
|
|
47
|
+
z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
|
|
48
|
+
attempts += 1;
|
|
49
|
+
const counter = document.getElementById("attempts");
|
|
50
|
+
if (counter) counter.textContent = String(attempts);
|
|
51
|
+
if (new URLSearchParams(location.search).get("scenario") === "refused")
|
|
52
|
+
return Promise.resolve({
|
|
53
|
+
error: "Only the owner can delete repositories",
|
|
54
|
+
});
|
|
55
|
+
withdrawn = true;
|
|
56
|
+
return Promise.resolve(
|
|
57
|
+
attempts === 1
|
|
58
|
+
? { error: "Provider temporarily unavailable; retry deletion" }
|
|
59
|
+
: { deleted: true },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (method === "repos.get")
|
|
63
|
+
return Promise.resolve(withdrawn ? { error: "No repository" } : detail);
|
|
64
|
+
if (method === "issues.read")
|
|
65
|
+
return Promise.resolve({ error: "Repository has no commits" });
|
|
66
|
+
if (method === "repos.list")
|
|
67
|
+
return Promise.resolve({ repos: withdrawn ? [] : [detail.repo] });
|
|
68
|
+
if (method === "activity.list") return Promise.resolve({ events: [] });
|
|
69
|
+
if (method === "changes.list") return Promise.resolve({ changes: [] });
|
|
70
|
+
if (method === "actors.list") return Promise.resolve({ agents: [] });
|
|
71
|
+
return Promise.reject(new Error(`Unexpected fixture method: ${method}`));
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
connect({ capabilities });
|
|
75
|
+
|
|
76
|
+
function Shell() {
|
|
77
|
+
const { route, go } = useRouter();
|
|
78
|
+
const opened = useRef(false);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!opened.current) {
|
|
81
|
+
opened.current = true;
|
|
82
|
+
go(`/${name}`);
|
|
83
|
+
}
|
|
84
|
+
}, [go]);
|
|
85
|
+
return (
|
|
86
|
+
<div className="caps-app-canvas bonsai-shell">
|
|
87
|
+
<main className="bonsai-page">
|
|
88
|
+
{route.page === "repo" ? (
|
|
89
|
+
<RepoPage route={route} live={live} />
|
|
90
|
+
) : (
|
|
91
|
+
<h1>Repositories</h1>
|
|
92
|
+
)}
|
|
93
|
+
</main>
|
|
94
|
+
<output id="attempts" hidden>
|
|
95
|
+
0
|
|
96
|
+
</output>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const root = document.getElementById("root");
|
|
101
|
+
if (!root) throw new Error("Settings fixture root missing");
|
|
102
|
+
createRoot(root).render(
|
|
103
|
+
<QueryClientProvider client={client}>
|
|
104
|
+
<RouterProvider>
|
|
105
|
+
<Shell />
|
|
106
|
+
</RouterProvider>
|
|
107
|
+
</QueryClientProvider>,
|
|
108
|
+
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import console from "node:console";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath, URL } from "node:url";
|
|
6
|
+
import { createServer } from "vite";
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
|
+
import viteConfig from "./vite.config.mjs";
|
|
9
|
+
|
|
10
|
+
const output = new URL(
|
|
11
|
+
"../../../../.botanical/repos-delete/shots/",
|
|
12
|
+
import.meta.url,
|
|
13
|
+
);
|
|
14
|
+
await mkdir(output, { recursive: true });
|
|
15
|
+
const server = await createServer({
|
|
16
|
+
...viteConfig,
|
|
17
|
+
configFile: false,
|
|
18
|
+
server: { ...viteConfig.server, port: 0 },
|
|
19
|
+
});
|
|
20
|
+
await server.listen();
|
|
21
|
+
const origin = server.resolvedUrls.local[0];
|
|
22
|
+
let browser;
|
|
23
|
+
const name = "disposable-merge-1791840651624";
|
|
24
|
+
const before = process.argv.includes("--before");
|
|
25
|
+
try {
|
|
26
|
+
browser = await chromium.launch({ headless: true });
|
|
27
|
+
for (const width of [1440, 390])
|
|
28
|
+
for (const theme of ["light", "dark"]) {
|
|
29
|
+
const page = await browser.newPage({
|
|
30
|
+
viewport: { width, height: 1000 },
|
|
31
|
+
colorScheme: theme,
|
|
32
|
+
});
|
|
33
|
+
page.setDefaultTimeout(10000);
|
|
34
|
+
const errors = [];
|
|
35
|
+
page.on("pageerror", (cause) => errors.push(cause.message));
|
|
36
|
+
await page.goto(`${origin}test/browser/repository-settings.html`);
|
|
37
|
+
await page.evaluate(
|
|
38
|
+
(value) =>
|
|
39
|
+
globalThis.document.documentElement.setAttribute("data-theme", value),
|
|
40
|
+
theme,
|
|
41
|
+
);
|
|
42
|
+
await page.getByRole("heading", { name, exact: true }).waitFor();
|
|
43
|
+
const settings = page.getByRole("link", {
|
|
44
|
+
name: "Settings",
|
|
45
|
+
exact: true,
|
|
46
|
+
});
|
|
47
|
+
if (before) {
|
|
48
|
+
assert.equal(await settings.count(), 0);
|
|
49
|
+
await page.screenshot({
|
|
50
|
+
path: fileURLToPath(new URL(`${width}-${theme}-before.png`, output)),
|
|
51
|
+
});
|
|
52
|
+
await page.close();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
await settings.click();
|
|
56
|
+
await page
|
|
57
|
+
.getByRole("heading", { name: "Repository settings", exact: true })
|
|
58
|
+
.waitFor();
|
|
59
|
+
assert.equal(await page.locator("#attempts").textContent(), "0");
|
|
60
|
+
await page.screenshot({
|
|
61
|
+
path: fileURLToPath(new URL(`${width}-${theme}-settings.png`, output)),
|
|
62
|
+
});
|
|
63
|
+
await page
|
|
64
|
+
.getByRole("button", { name: "Delete repository", exact: true })
|
|
65
|
+
.click();
|
|
66
|
+
await page
|
|
67
|
+
.getByRole("textbox", { name: "Repository name" })
|
|
68
|
+
.fill("wrong");
|
|
69
|
+
assert.equal(
|
|
70
|
+
await page
|
|
71
|
+
.getByRole("button", { name: "Confirm deletion" })
|
|
72
|
+
.isDisabled(),
|
|
73
|
+
true,
|
|
74
|
+
);
|
|
75
|
+
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
76
|
+
assert.equal(await page.locator("#attempts").textContent(), "0");
|
|
77
|
+
await page
|
|
78
|
+
.getByRole("button", { name: "Delete repository", exact: true })
|
|
79
|
+
.click();
|
|
80
|
+
await page.getByRole("textbox", { name: "Repository name" }).fill(name);
|
|
81
|
+
await page.screenshot({
|
|
82
|
+
path: fileURLToPath(new URL(`${width}-${theme}-confirm.png`, output)),
|
|
83
|
+
});
|
|
84
|
+
await page.getByRole("button", { name: "Confirm deletion" }).click();
|
|
85
|
+
await page
|
|
86
|
+
.getByRole("alert")
|
|
87
|
+
.filter({
|
|
88
|
+
hasText: "Deletion didn't finish because storage was unavailable.",
|
|
89
|
+
})
|
|
90
|
+
.waitFor();
|
|
91
|
+
assert.equal(await page.getByRole("alert").locator("p").count(), 1);
|
|
92
|
+
assert.equal(await page.locator("#attempts").textContent(), "1");
|
|
93
|
+
await page.screenshot({
|
|
94
|
+
path: fileURLToPath(new URL(`${width}-${theme}-retry.png`, output)),
|
|
95
|
+
});
|
|
96
|
+
await page.getByRole("button", { name: "Confirm deletion" }).click();
|
|
97
|
+
await page
|
|
98
|
+
.getByRole("heading", { name: "Repositories", exact: true })
|
|
99
|
+
.waitFor();
|
|
100
|
+
assert.equal(await page.locator("#attempts").textContent(), "2");
|
|
101
|
+
await page.goto(
|
|
102
|
+
`${origin}test/browser/repository-settings.html?scenario=refused`,
|
|
103
|
+
);
|
|
104
|
+
await page.getByRole("link", { name: "Settings", exact: true }).click();
|
|
105
|
+
await page
|
|
106
|
+
.getByRole("button", { name: "Delete repository", exact: true })
|
|
107
|
+
.click();
|
|
108
|
+
await page.getByRole("textbox", { name: "Repository name" }).fill(name);
|
|
109
|
+
await page.getByRole("button", { name: "Confirm deletion" }).click();
|
|
110
|
+
await page
|
|
111
|
+
.getByRole("alert")
|
|
112
|
+
.filter({ hasText: "Only the owner" })
|
|
113
|
+
.waitFor();
|
|
114
|
+
assert.deepEqual(errors, []);
|
|
115
|
+
assert.equal(
|
|
116
|
+
await page.evaluate(
|
|
117
|
+
() =>
|
|
118
|
+
globalThis.document.documentElement.scrollWidth <=
|
|
119
|
+
globalThis.innerWidth,
|
|
120
|
+
),
|
|
121
|
+
true,
|
|
122
|
+
"no horizontal overflow",
|
|
123
|
+
);
|
|
124
|
+
await page.close();
|
|
125
|
+
}
|
|
126
|
+
console.log(
|
|
127
|
+
before
|
|
128
|
+
? "PASS baseline 1440/390 light/dark: repository view before Settings"
|
|
129
|
+
: "PASS Settings 1440/390 light/dark: launcher entry, explicit confirmation, cancel, owner refusal, failed provider retry and home navigation",
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
await browser?.close();
|
|
133
|
+
await server.close();
|
|
134
|
+
console.log("CLOSED browser and Vite server");
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 0;
|