@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,234 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { URL } from "node:url";
|
|
5
|
+
import { Script } from "node:vm";
|
|
6
|
+
import * as S from "effect/Schema";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import {
|
|
9
|
+
catalogEvents,
|
|
10
|
+
catalogMethods,
|
|
11
|
+
compile,
|
|
12
|
+
triggerRun,
|
|
13
|
+
validate,
|
|
14
|
+
workflowDefinitionSchema,
|
|
15
|
+
} from "@fungi.computer/braid";
|
|
16
|
+
import { buildAppApi } from "@fungi.computer/agar/backend-declaration";
|
|
17
|
+
import { activityEnvelope } from "../shared/hooks.ts";
|
|
18
|
+
import { appBackend as bonsaiBackend } from "../backend/methods.ts";
|
|
19
|
+
const bonsaiApi = buildAppApi(bonsaiBackend);
|
|
20
|
+
|
|
21
|
+
// The stock Workflows template that builds and installs an App on merge.
|
|
22
|
+
async function buildOnMergeTemplate() {
|
|
23
|
+
const template = z
|
|
24
|
+
.object({ definition: z.json() })
|
|
25
|
+
.parse(
|
|
26
|
+
JSON.parse(
|
|
27
|
+
await readFile(
|
|
28
|
+
new URL(
|
|
29
|
+
"../../trellis/templates/bonsai-build-on-merge.json",
|
|
30
|
+
import.meta.url,
|
|
31
|
+
),
|
|
32
|
+
"utf8",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
return S.decodeUnknownSync(workflowDefinitionSchema)(template.definition);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function installedCatalog() {
|
|
40
|
+
return {
|
|
41
|
+
app_bonsai: {
|
|
42
|
+
methods: (await catalogMethods(bonsaiApi.methods)).methods,
|
|
43
|
+
events: (await catalogEvents(bonsaiApi.events)).events,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findNode(definition, id) {
|
|
49
|
+
const node = definition.nodes.find((candidate) => candidate.id === id);
|
|
50
|
+
assert.ok(node, `missing node ${id}`);
|
|
51
|
+
return node;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Only the external Code execution boundary is replaced by Node VM here.
|
|
55
|
+
const evaluate = async (code, input) =>
|
|
56
|
+
z.json().parse(
|
|
57
|
+
await new Script(`(async () => { ${code} })()`).runInNewContext({
|
|
58
|
+
env: { INPUT: { read: () => Promise.resolve(input) } },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const after = "c".repeat(40);
|
|
63
|
+
// The envelope Bonsai's outbox emits when a Change merges into `repo`.
|
|
64
|
+
function mergedEnvelope({ repo = "my-app", ref = "main" } = {}) {
|
|
65
|
+
return activityEnvelope(
|
|
66
|
+
{
|
|
67
|
+
id: 41,
|
|
68
|
+
at: Date.UTC(2026, 9, 15, 4),
|
|
69
|
+
type: "merge",
|
|
70
|
+
repo,
|
|
71
|
+
actor: "morel",
|
|
72
|
+
data: {
|
|
73
|
+
change: {
|
|
74
|
+
repo,
|
|
75
|
+
number: 3,
|
|
76
|
+
sourceRepo: `${repo}--morel`,
|
|
77
|
+
sourceRef: "edit",
|
|
78
|
+
targetRef: ref,
|
|
79
|
+
head: "b".repeat(40),
|
|
80
|
+
},
|
|
81
|
+
from: `${repo}--morel`,
|
|
82
|
+
ref,
|
|
83
|
+
before: "a".repeat(40),
|
|
84
|
+
after,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: `${repo}--morel`,
|
|
89
|
+
canonical: repo,
|
|
90
|
+
cloneUrl: `https://bonsai.example/git/${repo}--morel.git`,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test("pins the build-on-merge template to Bonsai's maintained declarations", async () => {
|
|
96
|
+
const definition = await buildOnMergeTemplate();
|
|
97
|
+
const catalog = await installedCatalog();
|
|
98
|
+
assert.deepEqual(validate(definition, catalog), []);
|
|
99
|
+
const compiled = await compile(definition, catalog, {
|
|
100
|
+
bonsai: { appId: "app_bonsai", installationId: "bonsai" },
|
|
101
|
+
});
|
|
102
|
+
assert.ok(compiled.ok, JSON.stringify(compiled));
|
|
103
|
+
// No key, token or Git pass rides in the definition: every step is an App
|
|
104
|
+
// method call made as the arming owner.
|
|
105
|
+
const text = JSON.stringify(definition);
|
|
106
|
+
assert.doesNotMatch(text, /gitAccess|OYSTER_OWNER_KEY|Authorization|token/iu);
|
|
107
|
+
// The run starts the build of the merged commit, then waits on that job.
|
|
108
|
+
const build = findNode(definition, "build");
|
|
109
|
+
assert.ok(build.kind === "app");
|
|
110
|
+
assert.equal(build.method, "apps.build");
|
|
111
|
+
assert.deepEqual(build.values.sourceRevision, {
|
|
112
|
+
from: { node: "merged", port: "sourceRevision" },
|
|
113
|
+
});
|
|
114
|
+
const status = findNode(definition, "build-status");
|
|
115
|
+
assert.ok(status.kind === "app");
|
|
116
|
+
assert.equal(status.method, "apps.buildStatus");
|
|
117
|
+
assert.deepEqual(status.values.jobId, {
|
|
118
|
+
from: { node: "build", port: "jobId" },
|
|
119
|
+
});
|
|
120
|
+
assert.equal(
|
|
121
|
+
catalog.app_bonsai.methods["apps.buildStatus"].pending,
|
|
122
|
+
true,
|
|
123
|
+
"the Workflow waits on the job through a pending method",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("starts on a merge into the App's source main, at the merged commit", async () => {
|
|
128
|
+
const definition = await buildOnMergeTemplate();
|
|
129
|
+
const trigger = definition.triggers?.find(
|
|
130
|
+
(item) => item.on === "event" && item.event === "change.merged",
|
|
131
|
+
);
|
|
132
|
+
assert.ok(trigger);
|
|
133
|
+
const payload = mergedEnvelope();
|
|
134
|
+
const outcome = triggerRun(definition, trigger.id, {
|
|
135
|
+
eventId: payload.data.eventId,
|
|
136
|
+
payload,
|
|
137
|
+
});
|
|
138
|
+
assert.equal(outcome.kind, "start", JSON.stringify(outcome));
|
|
139
|
+
// The owner setting defaults to showing the Workflows review UI.
|
|
140
|
+
assert.equal(outcome.input.reviewBeforeInstall, true);
|
|
141
|
+
const merged = findNode(definition, "merged");
|
|
142
|
+
assert.ok(merged.kind === "code");
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
await evaluate(merged.source, {
|
|
145
|
+
payload: outcome.input.payload,
|
|
146
|
+
appId: outcome.input.appId,
|
|
147
|
+
}),
|
|
148
|
+
{ appId: outcome.input.appId, sourceRevision: `git:${after}` },
|
|
149
|
+
);
|
|
150
|
+
for (const other of [{ repo: "elsewhere" }, { ref: "release" }]) {
|
|
151
|
+
const skipped = mergedEnvelope(other);
|
|
152
|
+
assert.deepEqual(
|
|
153
|
+
triggerRun(definition, trigger.id, {
|
|
154
|
+
eventId: skipped.data.eventId,
|
|
155
|
+
payload: skipped,
|
|
156
|
+
}),
|
|
157
|
+
{ kind: "ignored" },
|
|
158
|
+
JSON.stringify(other),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("installs only a passing build of the merged commit", async () => {
|
|
164
|
+
const definition = await buildOnMergeTemplate();
|
|
165
|
+
const built = findNode(definition, "built");
|
|
166
|
+
assert.ok(built.kind === "code");
|
|
167
|
+
const sourceRevision = `git:${after}`;
|
|
168
|
+
const release = {
|
|
169
|
+
appId: "app_myapp",
|
|
170
|
+
releaseId: "rel_built",
|
|
171
|
+
artifact: { provenance: { sourceRevision } },
|
|
172
|
+
};
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
await evaluate(built.source, {
|
|
175
|
+
status: "succeeded",
|
|
176
|
+
detail: null,
|
|
177
|
+
release,
|
|
178
|
+
sourceRevision,
|
|
179
|
+
}),
|
|
180
|
+
{
|
|
181
|
+
release,
|
|
182
|
+
summary: `Built rel_built of app_myapp from ${sourceRevision}`,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
await assert.rejects(
|
|
186
|
+
evaluate(built.source, {
|
|
187
|
+
status: "failed",
|
|
188
|
+
detail: "tsc exited 2",
|
|
189
|
+
release: null,
|
|
190
|
+
sourceRevision,
|
|
191
|
+
}),
|
|
192
|
+
/The build did not pass: tsc exited 2/u,
|
|
193
|
+
);
|
|
194
|
+
await assert.rejects(
|
|
195
|
+
evaluate(built.source, {
|
|
196
|
+
status: "succeeded",
|
|
197
|
+
detail: null,
|
|
198
|
+
release,
|
|
199
|
+
sourceRevision: `git:${"d".repeat(40)}`,
|
|
200
|
+
}),
|
|
201
|
+
/not the merged commit/u,
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("routes the Release through review only when Review before install is on", async () => {
|
|
206
|
+
const definition = await buildOnMergeTemplate();
|
|
207
|
+
const gate = findNode(definition, "review-gate");
|
|
208
|
+
assert.ok(gate.kind === "condition");
|
|
209
|
+
assert.deepEqual(gate.test, {
|
|
210
|
+
op: "eq",
|
|
211
|
+
left: { from: { node: "$input", port: "reviewBeforeInstall" } },
|
|
212
|
+
right: { literal: true },
|
|
213
|
+
});
|
|
214
|
+
// The review is reached only from the gate's true outlet, automatic
|
|
215
|
+
// install only from its false one; install takes whichever ran.
|
|
216
|
+
const review = findNode(definition, "review");
|
|
217
|
+
assert.ok(review.kind === "review");
|
|
218
|
+
assert.deepEqual(review.show[0], { node: "to-review", port: "release" });
|
|
219
|
+
assert.deepEqual(findNode(definition, "to-review").fields.review, {
|
|
220
|
+
from: { node: "review-gate", port: "true" },
|
|
221
|
+
});
|
|
222
|
+
assert.deepEqual(findNode(definition, "automatic").fields.automatic, {
|
|
223
|
+
from: { node: "review-gate", port: "false" },
|
|
224
|
+
});
|
|
225
|
+
const install = findNode(definition, "install");
|
|
226
|
+
assert.ok(install.kind === "app");
|
|
227
|
+
assert.equal(install.method, "apps.install");
|
|
228
|
+
assert.deepEqual(install.values.release, {
|
|
229
|
+
coalesce: [
|
|
230
|
+
{ from: { node: "review", port: "approved" } },
|
|
231
|
+
{ from: { node: "automatic", port: "release" } },
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Graph } from "../shared/contract.js";
|
|
3
|
+
import { layoutGraph } from "../web/network-graph.js";
|
|
4
|
+
|
|
5
|
+
const h = (n: number) => n.toString(16).padStart(40, "0");
|
|
6
|
+
|
|
7
|
+
describe("network layout", () => {
|
|
8
|
+
it("puts shared history on the canonical lane and fork work on its own lane", () => {
|
|
9
|
+
const graph: Graph = {
|
|
10
|
+
lanes: [
|
|
11
|
+
{
|
|
12
|
+
id: "r:main",
|
|
13
|
+
repo: "r",
|
|
14
|
+
ref: "main",
|
|
15
|
+
owner: null,
|
|
16
|
+
head: h(1),
|
|
17
|
+
canonical: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "r--ada:main",
|
|
21
|
+
repo: "r--ada",
|
|
22
|
+
ref: "main",
|
|
23
|
+
owner: "ada",
|
|
24
|
+
head: h(3),
|
|
25
|
+
canonical: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
nodes: [
|
|
29
|
+
{
|
|
30
|
+
hash: h(3),
|
|
31
|
+
parents: [h(2)],
|
|
32
|
+
message: "c",
|
|
33
|
+
author: "ada",
|
|
34
|
+
agent: "ada",
|
|
35
|
+
origin: "r--ada",
|
|
36
|
+
committedAt: 3,
|
|
37
|
+
lanes: ["r--ada:main"],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
hash: h(2),
|
|
41
|
+
parents: [h(1)],
|
|
42
|
+
message: "b",
|
|
43
|
+
author: "ada",
|
|
44
|
+
agent: "ada",
|
|
45
|
+
origin: "r--ada",
|
|
46
|
+
committedAt: 2,
|
|
47
|
+
lanes: ["r--ada:main"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
hash: h(1),
|
|
51
|
+
parents: [],
|
|
52
|
+
message: "a",
|
|
53
|
+
author: "levi",
|
|
54
|
+
agent: null,
|
|
55
|
+
origin: null,
|
|
56
|
+
committedAt: 1,
|
|
57
|
+
lanes: ["r:main", "r--ada:main"],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
const layout = layoutGraph(graph, 40);
|
|
62
|
+
expect(layout.placed.get(h(1))?.row).toBe(0);
|
|
63
|
+
expect(layout.placed.get(h(2))?.row).toBe(1);
|
|
64
|
+
const xs = [h(1), h(2), h(3)].map(
|
|
65
|
+
(hash) => layout.placed.get(hash)?.x ?? -1,
|
|
66
|
+
);
|
|
67
|
+
expect(xs).toEqual([...xs].sort((x, y) => x - y));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("orders a parent before its child even when clocks disagree", () => {
|
|
71
|
+
const graph: Graph = {
|
|
72
|
+
lanes: [
|
|
73
|
+
{
|
|
74
|
+
id: "r:main",
|
|
75
|
+
repo: "r",
|
|
76
|
+
ref: "main",
|
|
77
|
+
owner: null,
|
|
78
|
+
head: h(2),
|
|
79
|
+
canonical: true,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
nodes: [
|
|
83
|
+
{
|
|
84
|
+
hash: h(2),
|
|
85
|
+
parents: [h(1)],
|
|
86
|
+
message: "child",
|
|
87
|
+
author: "x",
|
|
88
|
+
agent: null,
|
|
89
|
+
origin: null,
|
|
90
|
+
committedAt: 1,
|
|
91
|
+
lanes: ["r:main"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
hash: h(1),
|
|
95
|
+
parents: [],
|
|
96
|
+
message: "parent",
|
|
97
|
+
author: "x",
|
|
98
|
+
agent: null,
|
|
99
|
+
origin: null,
|
|
100
|
+
committedAt: 5,
|
|
101
|
+
lanes: ["r:main"],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
const layout = layoutGraph(graph, 40);
|
|
106
|
+
expect(layout.placed.get(h(1))?.x).toBeLessThan(
|
|
107
|
+
layout.placed.get(h(2))?.x ?? 0,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps a merged fork commit on its fork lane", () => {
|
|
112
|
+
const graph: Graph = {
|
|
113
|
+
lanes: [
|
|
114
|
+
{
|
|
115
|
+
id: "r:main",
|
|
116
|
+
repo: "r",
|
|
117
|
+
ref: "main",
|
|
118
|
+
owner: null,
|
|
119
|
+
head: h(3),
|
|
120
|
+
canonical: true,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "r--ada:main",
|
|
124
|
+
repo: "r--ada",
|
|
125
|
+
ref: "main",
|
|
126
|
+
owner: "ada",
|
|
127
|
+
head: h(2),
|
|
128
|
+
canonical: false,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
nodes: [
|
|
132
|
+
{
|
|
133
|
+
hash: h(3),
|
|
134
|
+
parents: [h(1), h(2)],
|
|
135
|
+
message: "merge",
|
|
136
|
+
author: "bonsai",
|
|
137
|
+
agent: "levi",
|
|
138
|
+
origin: "r",
|
|
139
|
+
committedAt: 3,
|
|
140
|
+
lanes: ["r:main"],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
hash: h(2),
|
|
144
|
+
parents: [h(1)],
|
|
145
|
+
message: "work",
|
|
146
|
+
author: "ada",
|
|
147
|
+
agent: "ada",
|
|
148
|
+
origin: "r--ada",
|
|
149
|
+
committedAt: 2,
|
|
150
|
+
lanes: ["r:main", "r--ada:main"],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
hash: h(1),
|
|
154
|
+
parents: [],
|
|
155
|
+
message: "start",
|
|
156
|
+
author: "levi",
|
|
157
|
+
agent: "levi",
|
|
158
|
+
origin: "r",
|
|
159
|
+
committedAt: 1,
|
|
160
|
+
lanes: ["r:main", "r--ada:main"],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
const layout = layoutGraph(graph, 40);
|
|
165
|
+
expect(layout.placed.get(h(2))?.row).toBe(1);
|
|
166
|
+
expect(layout.placed.get(h(3))?.row).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("places every parent below its children in the vertical rail, despite clock skew", async () => {
|
|
171
|
+
const { verticalHistory } = await import("../web/commit-tree.js");
|
|
172
|
+
const lane = {
|
|
173
|
+
id: "r:main",
|
|
174
|
+
repo: "r",
|
|
175
|
+
ref: "main",
|
|
176
|
+
owner: null,
|
|
177
|
+
head: h(2),
|
|
178
|
+
canonical: true,
|
|
179
|
+
};
|
|
180
|
+
const base = {
|
|
181
|
+
message: "commit",
|
|
182
|
+
author: "x",
|
|
183
|
+
agent: null,
|
|
184
|
+
origin: null,
|
|
185
|
+
lanes: [lane.id],
|
|
186
|
+
};
|
|
187
|
+
const graph: Graph = {
|
|
188
|
+
lanes: [lane],
|
|
189
|
+
nodes: [
|
|
190
|
+
{ ...base, hash: h(1), parents: [], committedAt: 50 },
|
|
191
|
+
{ ...base, hash: h(2), parents: [h(1)], committedAt: 1 },
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
const tree = verticalHistory(graph);
|
|
195
|
+
expect(tree.order.map((item) => item.node.hash)).toEqual([h(2), h(1)]);
|
|
196
|
+
expect(tree.byHash.get(h(1))?.y).toBeGreaterThan(
|
|
197
|
+
tree.byHash.get(h(2))?.y ?? 0,
|
|
198
|
+
);
|
|
199
|
+
expect(tree.width).toBeLessThan(150);
|
|
200
|
+
});
|