@fungi.computer/bonsai 0.0.0-stage → 0.1.0
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 +43 -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 +848 -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/present.test.ts +27 -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 +47 -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 +719 -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,401 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { countingGit } from "@fungi.computer/agar/read-copy";
|
|
4
|
+
import { commitFiles } from "@fungi.computer/agar/git-commit";
|
|
5
|
+
import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
|
|
6
|
+
import { realGit } from "@fungi.computer/agar/test-kit/git";
|
|
7
|
+
import { readCopyFrame, gitStore } from "../backend/git.js";
|
|
8
|
+
import { readRepositoryRefs } from "../backend/repository-copy.js";
|
|
9
|
+
import { ensureBonsaiTables, BonsaiState } from "../backend/state.js";
|
|
10
|
+
import {
|
|
11
|
+
readGraph,
|
|
12
|
+
readCommits,
|
|
13
|
+
readCommitDetail,
|
|
14
|
+
invalidateGraphCommitCopies,
|
|
15
|
+
graphCommitCopyKeys,
|
|
16
|
+
rebuildGraphCommitCopy,
|
|
17
|
+
} from "../backend/graph-commit-copies.js";
|
|
18
|
+
|
|
19
|
+
async function fixture() {
|
|
20
|
+
const provider = realGit();
|
|
21
|
+
const database = new DatabaseSync(":memory:");
|
|
22
|
+
const owner = fromNodeSqlite(database);
|
|
23
|
+
ensureBonsaiTables(owner.sql);
|
|
24
|
+
const state = new BonsaiState(owner.sql, owner.transactionSync);
|
|
25
|
+
const lifetime = new AbortController();
|
|
26
|
+
const counted = countingGit(provider.git, {
|
|
27
|
+
limit: 1000,
|
|
28
|
+
signal: lifetime.signal,
|
|
29
|
+
});
|
|
30
|
+
const bonsai = {
|
|
31
|
+
state,
|
|
32
|
+
store: gitStore(counted.git),
|
|
33
|
+
events: { emit: () => Promise.resolve() },
|
|
34
|
+
};
|
|
35
|
+
await provider.git.create("pond");
|
|
36
|
+
state.registerRepo({
|
|
37
|
+
name: "pond",
|
|
38
|
+
description: null,
|
|
39
|
+
parent: null,
|
|
40
|
+
owner: null,
|
|
41
|
+
});
|
|
42
|
+
const write = await commitFiles(provider.git, {
|
|
43
|
+
repository: "pond",
|
|
44
|
+
branch: "main",
|
|
45
|
+
expectedHead: null,
|
|
46
|
+
changes: [
|
|
47
|
+
{ path: "notes.txt", content: new TextEncoder().encode("Fish\n") },
|
|
48
|
+
],
|
|
49
|
+
message: "Add fish",
|
|
50
|
+
author: { name: "Morel", email: "morel@test.invalid" },
|
|
51
|
+
});
|
|
52
|
+
if (write.kind !== "committed") throw new Error("Fixture commit failed");
|
|
53
|
+
state.repositoryCopies.write("pond", write.commit, {
|
|
54
|
+
defaultBranch: "main",
|
|
55
|
+
refs: [{ name: "refs/heads/main", hash: write.commit }],
|
|
56
|
+
cloneUrl: "https://git.test/pond.git",
|
|
57
|
+
empty: false,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
provider,
|
|
61
|
+
bonsai,
|
|
62
|
+
hash: write.commit,
|
|
63
|
+
counted,
|
|
64
|
+
database,
|
|
65
|
+
owner,
|
|
66
|
+
close() {
|
|
67
|
+
lifetime.abort();
|
|
68
|
+
database.close();
|
|
69
|
+
provider.close();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("Graph screen reads the ready copy without any Git binding calls", async () => {
|
|
75
|
+
const f = await fixture();
|
|
76
|
+
try {
|
|
77
|
+
const first = await readCopyFrame(f.bonsai.store, (store) =>
|
|
78
|
+
readGraph({ ...f.bonsai, store }, "pond"),
|
|
79
|
+
);
|
|
80
|
+
expect(first.lanes.map((lane) => lane.ref)).toEqual(["main"]);
|
|
81
|
+
expect(first.nodes.map((node) => node.message)).toEqual(["Add fish"]);
|
|
82
|
+
const before = f.counted.total();
|
|
83
|
+
const reopened = {
|
|
84
|
+
...f.bonsai,
|
|
85
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
86
|
+
};
|
|
87
|
+
expect(
|
|
88
|
+
await readCopyFrame(reopened.store, (store) =>
|
|
89
|
+
readGraph({ ...reopened, store }, "pond"),
|
|
90
|
+
),
|
|
91
|
+
).toEqual(first);
|
|
92
|
+
expect(f.counted.total() - before).toBe(0);
|
|
93
|
+
} finally {
|
|
94
|
+
f.close();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("Commits screen reads a ready branch page without any Git calls after restart", async () => {
|
|
99
|
+
const f = await fixture();
|
|
100
|
+
try {
|
|
101
|
+
const input = { repo: "pond", ref: "main" };
|
|
102
|
+
const first = await readCopyFrame(f.bonsai.store, (store) =>
|
|
103
|
+
readCommits({ ...f.bonsai, store }, input),
|
|
104
|
+
);
|
|
105
|
+
expect(first.commits.map((item) => item.commit.message)).toEqual([
|
|
106
|
+
"Add fish",
|
|
107
|
+
]);
|
|
108
|
+
const before = f.counted.total();
|
|
109
|
+
const reopened = {
|
|
110
|
+
...f.bonsai,
|
|
111
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
112
|
+
};
|
|
113
|
+
expect(
|
|
114
|
+
await readCopyFrame(reopened.store, (store) =>
|
|
115
|
+
readCommits({ ...reopened, store }, input),
|
|
116
|
+
),
|
|
117
|
+
).toEqual(first);
|
|
118
|
+
expect(f.counted.total() - before).toBe(0);
|
|
119
|
+
} finally {
|
|
120
|
+
f.close();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("Commit detail keeps its first-parent diff locally across restart", async () => {
|
|
125
|
+
const f = await fixture();
|
|
126
|
+
try {
|
|
127
|
+
const input = { repo: "pond", hash: f.hash };
|
|
128
|
+
const first = await readCopyFrame(f.bonsai.store, (store) =>
|
|
129
|
+
readCommitDetail({ ...f.bonsai, store }, input),
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
first.diff.files.map((file) => ({
|
|
133
|
+
path: file.path,
|
|
134
|
+
newText: file.newText,
|
|
135
|
+
})),
|
|
136
|
+
).toEqual([{ path: "notes.txt", newText: "Fish\n" }]);
|
|
137
|
+
const before = f.counted.total();
|
|
138
|
+
const reopened = {
|
|
139
|
+
...f.bonsai,
|
|
140
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
141
|
+
};
|
|
142
|
+
expect(
|
|
143
|
+
await readCopyFrame(reopened.store, (store) =>
|
|
144
|
+
readCommitDetail({ ...reopened, store }, input),
|
|
145
|
+
),
|
|
146
|
+
).toEqual(first);
|
|
147
|
+
expect(f.counted.total() - before).toBe(0);
|
|
148
|
+
} finally {
|
|
149
|
+
f.close();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("push invalidation refuses old ready refs when earlier preparation exhausts the frame", async () => {
|
|
154
|
+
const f = await fixture();
|
|
155
|
+
try {
|
|
156
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
157
|
+
readGraph({ ...f.bonsai, store }, "pond"),
|
|
158
|
+
);
|
|
159
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
160
|
+
readCommits({ ...f.bonsai, store }, { repo: "pond", ref: "main" }),
|
|
161
|
+
);
|
|
162
|
+
f.bonsai.state.invalidateRepositoryRefs("pond");
|
|
163
|
+
invalidateGraphCommitCopies(f.bonsai, "pond");
|
|
164
|
+
const before = f.counted.total();
|
|
165
|
+
await expect(
|
|
166
|
+
readCopyFrame(f.bonsai.store, async (store) => {
|
|
167
|
+
for (let i = 0; i < 40; i++) await store.info("pond");
|
|
168
|
+
await store.info("pond");
|
|
169
|
+
}),
|
|
170
|
+
).rejects.toThrow("budget exhausted");
|
|
171
|
+
expect(f.counted.total() - before).toBe(40);
|
|
172
|
+
const reopened = {
|
|
173
|
+
...f.bonsai,
|
|
174
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
175
|
+
};
|
|
176
|
+
await expect(
|
|
177
|
+
readCopyFrame(reopened.store, (store) =>
|
|
178
|
+
readGraph({ ...reopened, store }, "pond"),
|
|
179
|
+
),
|
|
180
|
+
).rejects.toMatchObject({ code: "unavailable" });
|
|
181
|
+
await expect(
|
|
182
|
+
readCopyFrame(reopened.store, (store) =>
|
|
183
|
+
readCommits({ ...reopened, store }, { repo: "pond", ref: "main" }),
|
|
184
|
+
),
|
|
185
|
+
).rejects.toMatchObject({ code: "unavailable" });
|
|
186
|
+
} finally {
|
|
187
|
+
f.close();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("Graph includes canonical and fork refs without Git when ready", async () => {
|
|
192
|
+
const f = await fixture();
|
|
193
|
+
try {
|
|
194
|
+
await f.provider.git.create("pond--morel");
|
|
195
|
+
f.bonsai.state.registerRepo({
|
|
196
|
+
name: "pond--morel",
|
|
197
|
+
description: null,
|
|
198
|
+
parent: "pond",
|
|
199
|
+
owner: "morel",
|
|
200
|
+
});
|
|
201
|
+
const write = await commitFiles(f.provider.git, {
|
|
202
|
+
repository: "pond--morel",
|
|
203
|
+
branch: "work",
|
|
204
|
+
expectedHead: null,
|
|
205
|
+
changes: [
|
|
206
|
+
{ path: "notes.txt", content: new TextEncoder().encode("Fork fish\n") },
|
|
207
|
+
],
|
|
208
|
+
message: "Fork work",
|
|
209
|
+
author: { name: "Morel", email: "morel@test.invalid" },
|
|
210
|
+
});
|
|
211
|
+
if (write.kind !== "committed") throw Error("Fork fixture commit failed");
|
|
212
|
+
f.bonsai.state.repositoryCopies.write("pond--morel", write.commit, {
|
|
213
|
+
defaultBranch: "work",
|
|
214
|
+
refs: [{ name: "refs/heads/work", hash: write.commit }],
|
|
215
|
+
cloneUrl: "https://git.test/pond--morel.git",
|
|
216
|
+
empty: false,
|
|
217
|
+
});
|
|
218
|
+
const first = await readCopyFrame(f.bonsai.store, (store) =>
|
|
219
|
+
readGraph({ ...f.bonsai, store }, "pond"),
|
|
220
|
+
);
|
|
221
|
+
expect(
|
|
222
|
+
first.lanes.map((lane) => ({
|
|
223
|
+
repo: lane.repo,
|
|
224
|
+
ref: lane.ref,
|
|
225
|
+
owner: lane.owner,
|
|
226
|
+
})),
|
|
227
|
+
).toEqual([
|
|
228
|
+
{ repo: "pond", ref: "main", owner: null },
|
|
229
|
+
{ repo: "pond--morel", ref: "work", owner: "morel" },
|
|
230
|
+
]);
|
|
231
|
+
expect(new Set(first.nodes.map((node) => node.message))).toEqual(
|
|
232
|
+
new Set(["Add fish", "Fork work"]),
|
|
233
|
+
);
|
|
234
|
+
const before = f.counted.total();
|
|
235
|
+
const reopened = {
|
|
236
|
+
...f.bonsai,
|
|
237
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
238
|
+
};
|
|
239
|
+
expect(
|
|
240
|
+
await readCopyFrame(reopened.store, (store) =>
|
|
241
|
+
readGraph({ ...reopened, store }, "pond"),
|
|
242
|
+
),
|
|
243
|
+
).toEqual(first);
|
|
244
|
+
expect(f.counted.total() - before).toBe(0);
|
|
245
|
+
} finally {
|
|
246
|
+
f.close();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("ref deletion removes the lane and branch history while immutable detail stays readable", async () => {
|
|
251
|
+
const f = await fixture();
|
|
252
|
+
try {
|
|
253
|
+
const detail = await readCopyFrame(f.bonsai.store, (store) =>
|
|
254
|
+
readCommitDetail({ ...f.bonsai, store }, { repo: "pond", hash: f.hash }),
|
|
255
|
+
);
|
|
256
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
257
|
+
readGraph({ ...f.bonsai, store }, "pond"),
|
|
258
|
+
);
|
|
259
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
260
|
+
readCommits({ ...f.bonsai, store }, { repo: "pond", ref: "main" }),
|
|
261
|
+
);
|
|
262
|
+
invalidateGraphCommitCopies(f.bonsai, "pond");
|
|
263
|
+
f.bonsai.state.repositoryCopies.write("pond", null, {
|
|
264
|
+
defaultBranch: "main",
|
|
265
|
+
refs: [],
|
|
266
|
+
cloneUrl: "https://git.test/pond.git",
|
|
267
|
+
empty: true,
|
|
268
|
+
});
|
|
269
|
+
const before = f.counted.total();
|
|
270
|
+
expect(
|
|
271
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
272
|
+
readGraph({ ...f.bonsai, store }, "pond"),
|
|
273
|
+
),
|
|
274
|
+
).toEqual({ lanes: [], nodes: [] });
|
|
275
|
+
expect(
|
|
276
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
277
|
+
readCommits({ ...f.bonsai, store }, { repo: "pond", ref: "main" }),
|
|
278
|
+
),
|
|
279
|
+
).toEqual({ ref: "main", commits: [] });
|
|
280
|
+
expect(
|
|
281
|
+
await readCopyFrame(f.bonsai.store, (store) =>
|
|
282
|
+
readCommitDetail(
|
|
283
|
+
{ ...f.bonsai, store },
|
|
284
|
+
{ repo: "pond", hash: f.hash },
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
).toEqual(detail);
|
|
288
|
+
expect(f.counted.total() - before).toBe(0);
|
|
289
|
+
} finally {
|
|
290
|
+
f.close();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("foreground keys finish a 60-file commit in bounded frames with durable object reuse", async () => {
|
|
295
|
+
const f = await fixture();
|
|
296
|
+
try {
|
|
297
|
+
const write = await commitFiles(f.provider.git, {
|
|
298
|
+
repository: "pond",
|
|
299
|
+
branch: "main",
|
|
300
|
+
expectedHead: f.hash,
|
|
301
|
+
changes: Array.from({ length: 60 }, (_, index) => ({
|
|
302
|
+
path: `fish-${String(index)}.txt`,
|
|
303
|
+
content: new TextEncoder().encode(`Pond fish ${String(index)}\n`),
|
|
304
|
+
})),
|
|
305
|
+
message: "Add 60 fish",
|
|
306
|
+
author: { name: "Morel", email: "morel@test.invalid" },
|
|
307
|
+
});
|
|
308
|
+
if (write.kind !== "committed") throw Error("Large fixture commit failed");
|
|
309
|
+
f.bonsai.state.repositoryCopies.write("pond", write.commit, {
|
|
310
|
+
defaultBranch: "main",
|
|
311
|
+
refs: [{ name: "refs/heads/main", hash: write.commit }],
|
|
312
|
+
cloneUrl: "https://git.test/pond.git",
|
|
313
|
+
empty: false,
|
|
314
|
+
});
|
|
315
|
+
const keys = graphCommitCopyKeys(f.bonsai, "pond");
|
|
316
|
+
let index = 0,
|
|
317
|
+
frames = 0,
|
|
318
|
+
unfinished = 0;
|
|
319
|
+
while (index < keys.length && frames < 10) {
|
|
320
|
+
const key = keys[index];
|
|
321
|
+
if (key === undefined) throw Error("Missing rebuild key");
|
|
322
|
+
const before = f.counted.total();
|
|
323
|
+
const runtime = {
|
|
324
|
+
...f.bonsai,
|
|
325
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
326
|
+
};
|
|
327
|
+
const done = await readCopyFrame(runtime.store, (store) =>
|
|
328
|
+
rebuildGraphCommitCopy({ ...runtime, store }, key),
|
|
329
|
+
);
|
|
330
|
+
expect(f.counted.total() - before).toBeLessThanOrEqual(40);
|
|
331
|
+
if (done) index++;
|
|
332
|
+
else unfinished++;
|
|
333
|
+
frames++;
|
|
334
|
+
}
|
|
335
|
+
expect(index).toBe(keys.length);
|
|
336
|
+
expect(unfinished).toBeGreaterThan(0);
|
|
337
|
+
const before = f.counted.total();
|
|
338
|
+
const detail = await readCopyFrame(f.bonsai.store, (store) =>
|
|
339
|
+
readCommitDetail(
|
|
340
|
+
{ ...f.bonsai, store },
|
|
341
|
+
{ repo: "pond", hash: write.commit },
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
expect(detail.diff.files).toHaveLength(60);
|
|
345
|
+
expect(detail.diff.additions).toBe(60);
|
|
346
|
+
expect(f.counted.total() - before).toBe(0);
|
|
347
|
+
} finally {
|
|
348
|
+
f.close();
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("only the current refs refresh can clear pending after facet reconstruction", async () => {
|
|
353
|
+
const f = await fixture();
|
|
354
|
+
try {
|
|
355
|
+
const value = readRepositoryRefs(f.bonsai, "pond");
|
|
356
|
+
const older = f.bonsai.state.invalidateRepositoryRefs("pond");
|
|
357
|
+
const newer = f.bonsai.state.invalidateRepositoryRefs("pond");
|
|
358
|
+
const reopened = {
|
|
359
|
+
...f.bonsai,
|
|
360
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
361
|
+
};
|
|
362
|
+
expect(
|
|
363
|
+
reopened.state.publishRepositoryRefs("pond", older, f.hash, value),
|
|
364
|
+
).toBe(false);
|
|
365
|
+
expect(() => readRepositoryRefs(reopened, "pond")).toThrow(
|
|
366
|
+
"Repository refs are rebuilding",
|
|
367
|
+
);
|
|
368
|
+
expect(
|
|
369
|
+
reopened.state.publishRepositoryRefs("pond", newer, f.hash, value),
|
|
370
|
+
).toBe(true);
|
|
371
|
+
const before = f.counted.total();
|
|
372
|
+
expect(readRepositoryRefs(reopened, "pond")).toEqual(value);
|
|
373
|
+
expect(f.counted.total() - before).toBe(0);
|
|
374
|
+
} finally {
|
|
375
|
+
f.close();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("pending mutable refs do not block an immutable commit detail", async () => {
|
|
380
|
+
const f = await fixture();
|
|
381
|
+
try {
|
|
382
|
+
const input = { repo: "pond", hash: f.hash };
|
|
383
|
+
const detail = await readCopyFrame(f.bonsai.store, (store) =>
|
|
384
|
+
readCommitDetail({ ...f.bonsai, store }, input),
|
|
385
|
+
);
|
|
386
|
+
f.bonsai.state.invalidateRepositoryRefs("pond");
|
|
387
|
+
const reopened = {
|
|
388
|
+
...f.bonsai,
|
|
389
|
+
state: new BonsaiState(f.owner.sql, f.owner.transactionSync),
|
|
390
|
+
};
|
|
391
|
+
const before = f.counted.total();
|
|
392
|
+
expect(
|
|
393
|
+
await readCopyFrame(reopened.store, (store) =>
|
|
394
|
+
readCommitDetail({ ...reopened, store }, input),
|
|
395
|
+
),
|
|
396
|
+
).toEqual(detail);
|
|
397
|
+
expect(f.counted.total() - before).toBe(0);
|
|
398
|
+
} finally {
|
|
399
|
+
f.close();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { act, createElement } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import {
|
|
6
|
+
QueryClient,
|
|
7
|
+
QueryClientProvider,
|
|
8
|
+
notifyManager,
|
|
9
|
+
} from "@tanstack/react-query";
|
|
10
|
+
import { createCapabilityRegistry } from "@fungi.computer/agar";
|
|
11
|
+
import { appMethods } from "@fungi.computer/agar/backend";
|
|
12
|
+
import { HomePage } from "../web/home-page.js";
|
|
13
|
+
import { RouterProvider } from "../web/router.js";
|
|
14
|
+
import { connect } from "../web/transport.js";
|
|
15
|
+
import type { JsonValue } from "../shared/contract.js";
|
|
16
|
+
|
|
17
|
+
const callMethod =
|
|
18
|
+
vi.fn<(method: string, input: string) => Promise<JsonValue>>();
|
|
19
|
+
|
|
20
|
+
let container: HTMLDivElement;
|
|
21
|
+
let root: ReturnType<typeof createRoot>;
|
|
22
|
+
let client: QueryClient;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.stubGlobal("scrollTo", vi.fn());
|
|
26
|
+
Object.defineProperty(HTMLElement.prototype, "scrollTo", {
|
|
27
|
+
configurable: true,
|
|
28
|
+
value: vi.fn(),
|
|
29
|
+
});
|
|
30
|
+
vi.stubGlobal(
|
|
31
|
+
"matchMedia",
|
|
32
|
+
vi.fn(() => ({
|
|
33
|
+
matches: false,
|
|
34
|
+
addEventListener: vi.fn(),
|
|
35
|
+
removeEventListener: vi.fn(),
|
|
36
|
+
})),
|
|
37
|
+
);
|
|
38
|
+
vi.stubGlobal(
|
|
39
|
+
"ResizeObserver",
|
|
40
|
+
class {
|
|
41
|
+
observe = vi.fn();
|
|
42
|
+
disconnect = vi.fn();
|
|
43
|
+
unobserve = vi.fn();
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
const capabilities = createCapabilityRegistry();
|
|
47
|
+
capabilities.provide(appMethods, { call: callMethod });
|
|
48
|
+
connect({ capabilities });
|
|
49
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
50
|
+
notifyManager.setScheduler(queueMicrotask);
|
|
51
|
+
client = new QueryClient({
|
|
52
|
+
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
|
|
53
|
+
});
|
|
54
|
+
container = document.createElement("div");
|
|
55
|
+
document.body.appendChild(container);
|
|
56
|
+
root = createRoot(container);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
await act(async () => {
|
|
61
|
+
root.unmount();
|
|
62
|
+
await Promise.resolve();
|
|
63
|
+
});
|
|
64
|
+
connect(null);
|
|
65
|
+
client.clear();
|
|
66
|
+
container.remove();
|
|
67
|
+
vi.unstubAllGlobals();
|
|
68
|
+
vi.resetAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const render = async (child: ReturnType<typeof createElement>) => {
|
|
72
|
+
await act(async () => {
|
|
73
|
+
root.render(createElement(QueryClientProvider, { client }, child));
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("Bonsai home page", () => {
|
|
80
|
+
it("shows the live board and no README section", async () => {
|
|
81
|
+
callMethod.mockImplementation((method) => {
|
|
82
|
+
switch (method) {
|
|
83
|
+
case "repos.list":
|
|
84
|
+
return Promise.resolve({
|
|
85
|
+
repos: [
|
|
86
|
+
{
|
|
87
|
+
name: "koi-pond",
|
|
88
|
+
description: "A pond for koi",
|
|
89
|
+
parent: null,
|
|
90
|
+
owner: null,
|
|
91
|
+
createdAt: 1,
|
|
92
|
+
lastPushAt: null,
|
|
93
|
+
forks: [],
|
|
94
|
+
openChanges: 0,
|
|
95
|
+
activity: [],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
case "actors.list":
|
|
100
|
+
return Promise.resolve({ agents: [] });
|
|
101
|
+
case "changes.list":
|
|
102
|
+
return Promise.resolve({ changes: [] });
|
|
103
|
+
case "activity.list":
|
|
104
|
+
return Promise.resolve({
|
|
105
|
+
events: [
|
|
106
|
+
{
|
|
107
|
+
id: 1,
|
|
108
|
+
at: 1,
|
|
109
|
+
type: "repo.created",
|
|
110
|
+
repo: "koi-pond",
|
|
111
|
+
actor: "ada",
|
|
112
|
+
data: {},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
default:
|
|
117
|
+
return Promise.reject(new Error(`unexpected method ${method}`));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
await render(
|
|
121
|
+
createElement(
|
|
122
|
+
RouterProvider,
|
|
123
|
+
null,
|
|
124
|
+
createElement(HomePage, {
|
|
125
|
+
live: { connected: true, error: null, fresh: new Set<number>() },
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
// The window title already says "Bonsai"; the home page renders no
|
|
130
|
+
// repeated inner title.
|
|
131
|
+
expect(container.querySelector("h1")).toBeNull();
|
|
132
|
+
expect(container.textContent).toContain("koi-pond");
|
|
133
|
+
expect(container.querySelector('[data-tile="readme"]')).toBeNull();
|
|
134
|
+
expect(
|
|
135
|
+
[...container.querySelectorAll("h2")].some(
|
|
136
|
+
(heading) => heading.textContent === "README",
|
|
137
|
+
),
|
|
138
|
+
).toBe(false);
|
|
139
|
+
expect(container.textContent).not.toContain("Bonsai is a git platform");
|
|
140
|
+
});
|
|
141
|
+
it("shows its empty state on a fresh Oyster with no repositories", async () => {
|
|
142
|
+
callMethod.mockImplementation((method) => {
|
|
143
|
+
switch (method) {
|
|
144
|
+
case "repos.list":
|
|
145
|
+
return Promise.resolve({ repos: [] });
|
|
146
|
+
case "actors.list":
|
|
147
|
+
return Promise.resolve({ agents: [] });
|
|
148
|
+
case "activity.list":
|
|
149
|
+
return Promise.resolve({ events: [] });
|
|
150
|
+
default:
|
|
151
|
+
return Promise.reject(new Error(`unexpected method ${method}`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
await render(
|
|
155
|
+
createElement(
|
|
156
|
+
RouterProvider,
|
|
157
|
+
null,
|
|
158
|
+
createElement(HomePage, {
|
|
159
|
+
live: { connected: true, error: null, fresh: new Set<number>() },
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
expect(
|
|
164
|
+
[...container.querySelectorAll("h2")].map(
|
|
165
|
+
(heading) => heading.textContent,
|
|
166
|
+
),
|
|
167
|
+
).toEqual(["No repositories yet"]);
|
|
168
|
+
expect(container.textContent).not.toContain("Loading repositories");
|
|
169
|
+
});
|
|
170
|
+
});
|