@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.81
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 +15 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
|
@@ -1,680 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
filesChangedSince,
|
|
18
|
-
hasAnyBuildFailure,
|
|
19
|
-
hasBuildFailureForGeneration,
|
|
20
|
-
isLegacyBackendFallbackFile,
|
|
21
|
-
isRssCeilingUnreachable,
|
|
22
|
-
mergeBackendRestartReasons,
|
|
23
|
-
mergeInvalidateMessages,
|
|
24
|
-
normalizeBackendReportedGeneration,
|
|
25
|
-
resolveIdleSuspendMs,
|
|
26
|
-
shouldAbandonBackendRecovery,
|
|
27
|
-
shouldHoldForReturningBuilder,
|
|
28
|
-
shouldMarkBuildPhaseRecovered,
|
|
29
|
-
shouldQueueBuildStatusReplay,
|
|
30
|
-
shouldRefreshConfigOnIdleWake,
|
|
31
|
-
shouldRelayRecycledFrontendState,
|
|
32
|
-
shouldReplaceLastGoodMessage,
|
|
33
|
-
shouldRestartBackendByDevPlan,
|
|
34
|
-
shouldRestartBuilderByDevPlan,
|
|
35
|
-
shouldRestartDevHostByDevPlan,
|
|
36
|
-
shouldWarnBuilderRssCeilingTight,
|
|
37
|
-
} from "./akanApp.host";
|
|
38
|
-
|
|
39
|
-
const invalidateWithActions = (actions: DevChangeAction[]): Extract<BuilderMessage, { type: "invalidate" }> => ({
|
|
40
|
-
type: "invalidate",
|
|
41
|
-
kinds: ["code"],
|
|
42
|
-
files: ["/repo/libs/shared/common/foo.ts"],
|
|
43
|
-
generation: 1,
|
|
44
|
-
devPlan: {
|
|
45
|
-
generation: 1,
|
|
46
|
-
files: ["/repo/libs/shared/common/foo.ts"],
|
|
47
|
-
generatedFiles: [],
|
|
48
|
-
roles: ["shared"],
|
|
49
|
-
actions,
|
|
50
|
-
reasonByFile: {},
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("shouldRestartBackendByDevPlan", () => {
|
|
55
|
-
test("uses explicit restart-backend action", () => {
|
|
56
|
-
expect(shouldRestartBackendByDevPlan(invalidateWithActions(["restart-backend"]))).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("blocks restart for error generations", () => {
|
|
60
|
-
expect(shouldRestartBackendByDevPlan(invalidateWithActions(["restart-backend", "report-error"]))).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("does not use backend-only restart when builder recycle is required", () => {
|
|
64
|
-
expect(shouldRestartBackendByDevPlan(invalidateWithActions(["restart-backend", "restart-builder"]))).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("falls back when no devPlan is present", () => {
|
|
68
|
-
expect(
|
|
69
|
-
shouldRestartBackendByDevPlan({
|
|
70
|
-
type: "invalidate",
|
|
71
|
-
kinds: ["code"],
|
|
72
|
-
files: ["/repo/apps/akan/page/_index.tsx"],
|
|
73
|
-
}),
|
|
74
|
-
).toBeNull();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("shouldRestartDevHostByDevPlan", () => {
|
|
79
|
-
test("detects explicit restart-builder actions", () => {
|
|
80
|
-
expect(shouldRestartBuilderByDevPlan(invalidateWithActions(["restart-builder"]))).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("detects explicit restart-dev-host actions", () => {
|
|
84
|
-
expect(shouldRestartDevHostByDevPlan(invalidateWithActions(["restart-dev-host"]))).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("detects legacy config invalidates", () => {
|
|
88
|
-
expect(
|
|
89
|
-
shouldRestartDevHostByDevPlan({
|
|
90
|
-
type: "invalidate",
|
|
91
|
-
kinds: ["config"],
|
|
92
|
-
files: ["/repo/apps/akan/akan.config.ts"],
|
|
93
|
-
}),
|
|
94
|
-
).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("backend restart reason helpers", () => {
|
|
99
|
-
test("extracts restart roles and generation from devPlan", () => {
|
|
100
|
-
const message = invalidateWithActions(["restart-backend"]);
|
|
101
|
-
const devPlan = message.devPlan;
|
|
102
|
-
if (!devPlan) throw new Error("devPlan expected");
|
|
103
|
-
message.devPlan = {
|
|
104
|
-
...devPlan,
|
|
105
|
-
roles: ["client", "shared", "barrel", "css"],
|
|
106
|
-
generation: 7,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
expect(backendRestartReasonFromMessage(message)).toEqual({
|
|
110
|
-
generation: 7,
|
|
111
|
-
files: ["/repo/libs/shared/common/foo.ts"],
|
|
112
|
-
roles: ["shared", "barrel"],
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("merges debounced restart reasons without losing latest generation", () => {
|
|
117
|
-
expect(
|
|
118
|
-
mergeBackendRestartReasons(
|
|
119
|
-
{ generation: 12, files: ["/repo/b.ts", "/repo/a.ts"], roles: ["server"] },
|
|
120
|
-
{ generation: 13, files: ["/repo/b.ts", "/repo/c.ts"], roles: ["barrel", "shared"] },
|
|
121
|
-
),
|
|
122
|
-
).toEqual({
|
|
123
|
-
generation: 13,
|
|
124
|
-
files: ["/repo/a.ts", "/repo/b.ts", "/repo/c.ts"],
|
|
125
|
-
roles: ["server", "shared", "barrel"],
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("keeps newer pending generation when an older reason is merged later", () => {
|
|
130
|
-
expect(
|
|
131
|
-
mergeBackendRestartReasons(
|
|
132
|
-
{ generation: 14, files: ["/repo/newer.ts"], roles: ["shared"] },
|
|
133
|
-
{ generation: 13, files: ["/repo/older.ts"], roles: ["server"] },
|
|
134
|
-
).generation,
|
|
135
|
-
).toBe(14);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("last-good frontend helpers", () => {
|
|
140
|
-
const pagesUpdated = (
|
|
141
|
-
generation: number | undefined,
|
|
142
|
-
buildId: number,
|
|
143
|
-
): Extract<BuilderMessage, { type: "pages-updated" }> => ({
|
|
144
|
-
type: "pages-updated",
|
|
145
|
-
data: {
|
|
146
|
-
bundlePath: `/tmp/pages-${buildId}.js`,
|
|
147
|
-
buildId,
|
|
148
|
-
generation,
|
|
149
|
-
changedFiles: [],
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("accepts newer successful pages payloads", () => {
|
|
154
|
-
expect(shouldReplaceLastGoodMessage(pagesUpdated(10, 1), pagesUpdated(11, 2))).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("rejects stale successful pages payloads", () => {
|
|
158
|
-
expect(shouldReplaceLastGoodMessage(pagesUpdated(11, 2), pagesUpdated(10, 1))).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("holding requests for a returning builder", () => {
|
|
163
|
-
test("holds while the builder is on its way back", () => {
|
|
164
|
-
expect(shouldHoldForReturningBuilder({ status: "restarting", heldCount: 0 })).toBe(true);
|
|
165
|
-
expect(shouldHoldForReturningBuilder({ status: "starting", heldCount: 3 })).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("holds through the drain too, not only after the process is gone", () => {
|
|
169
|
-
// The window this decision originally missed: the builder is still alive and refusing, which is
|
|
170
|
-
// the same gap as a restart from anyone waiting on a page.
|
|
171
|
-
expect(shouldHoldForReturningBuilder({ status: "recycling", heldCount: 0 })).toBe(true);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("fails immediately when nothing is bringing the builder back", () => {
|
|
175
|
-
expect(shouldHoldForReturningBuilder({ status: "stopped", heldCount: 0 })).toBe(false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("stops holding once the queue is full", () => {
|
|
179
|
-
expect(shouldHoldForReturningBuilder({ status: "restarting", heldCount: 3, limit: 4 })).toBe(true);
|
|
180
|
-
expect(shouldHoldForReturningBuilder({ status: "restarting", heldCount: 4, limit: 4 })).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe("builder rss recycle", () => {
|
|
185
|
-
const ceiling = 1_200 * 1024 * 1024;
|
|
186
|
-
const decide = (over: Partial<Parameters<typeof decideBuilderRssRecycle>[0]>) =>
|
|
187
|
-
decideBuilderRssRecycle({
|
|
188
|
-
rssBytes: ceiling + 1,
|
|
189
|
-
ceilingBytes: ceiling,
|
|
190
|
-
buildFailed: false,
|
|
191
|
-
msSinceLastRecycle: null,
|
|
192
|
-
...over,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("recycles an idle builder that crossed the ceiling", () => {
|
|
196
|
-
expect(decide({})).toBe("recycle");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test("leaves a builder under the ceiling alone", () => {
|
|
200
|
-
expect(decide({ rssBytes: ceiling - 1 })).toBe("below-ceiling");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("does nothing when no ceiling is configured", () => {
|
|
204
|
-
expect(decide({ ceilingBytes: null })).toBe("unbounded");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Rebooting on a generation whose build failed strands the dev server: the replacement hits the
|
|
208
|
-
// same compile error and exits before builder-ready.
|
|
209
|
-
test("defers while the current generation has a failing build", () => {
|
|
210
|
-
expect(decide({ buildFailed: true })).toBe("build-failed");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("refuses a second recycle inside the minimum interval", () => {
|
|
214
|
-
expect(decide({ msSinceLastRecycle: 5_000 })).toBe("too-soon");
|
|
215
|
-
expect(decide({ msSinceLastRecycle: 31_000 })).toBe("recycle");
|
|
216
|
-
expect(decide({ msSinceLastRecycle: 5_000, minIntervalMs: 1_000 })).toBe("recycle");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Says so, and keeps enforcing: a page load is two route builds, so an app whose builds sit over the
|
|
220
|
-
// ceiling reaches this on its first navigation — which is normal work, not a reason to drop the only
|
|
221
|
-
// bound the builder has.
|
|
222
|
-
test("mentions a tight ceiling rather than acting on it", () => {
|
|
223
|
-
expect(shouldWarnBuilderRssCeilingTight(1)).toBe(false);
|
|
224
|
-
expect(shouldWarnBuilderRssCeilingTight(2)).toBe(true);
|
|
225
|
-
expect(shouldWarnBuilderRssCeilingTight(1, 1)).toBe(true);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// The one case recycling cannot fix, measured on the replacement before it has built anything: every
|
|
229
|
-
// future replacement lands on the same floor, so the loop would only ever cost boot builds.
|
|
230
|
-
test("gives up only when a fresh builder is already over the ceiling", () => {
|
|
231
|
-
expect(isRssCeilingUnreachable(ceiling + 1, ceiling)).toBe(true);
|
|
232
|
-
expect(isRssCeilingUnreachable(ceiling - 1, ceiling)).toBe(false);
|
|
233
|
-
// An unreadable rss is no information, and no ceiling is nothing to be unreachable.
|
|
234
|
-
expect(isRssCeilingUnreachable(null, ceiling)).toBe(false);
|
|
235
|
-
expect(isRssCeilingUnreachable(ceiling + 1, null)).toBe(false);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// Measured on Linux: the builder peaked at 522MiB and settled at 214MiB with no help, so a 400MiB
|
|
239
|
-
// ceiling recycled a process that was already back under it. The armed sample is always the peak.
|
|
240
|
-
describe("settle check before committing", () => {
|
|
241
|
-
test("waits when the builder is only modestly over the ceiling", () => {
|
|
242
|
-
expect(decideBuilderRssSettle({ rssBytes: 522, ceilingBytes: 400 })).toBe("wait-and-recheck");
|
|
243
|
-
expect(decideBuilderRssSettle({ rssBytes: 401, ceilingBytes: 400 })).toBe("wait-and-recheck");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// No purge is going to rescue a builder this far over, so waiting only delays the inevitable.
|
|
247
|
-
test("recycles immediately once far enough past the ceiling", () => {
|
|
248
|
-
expect(decideBuilderRssSettle({ rssBytes: 600, ceilingBytes: 400 })).toBe("recycle-now");
|
|
249
|
-
expect(decideBuilderRssSettle({ rssBytes: 900, ceilingBytes: 400 })).toBe("recycle-now");
|
|
250
|
-
expect(decideBuilderRssSettle({ rssBytes: 500, ceilingBytes: 400, hardMultiple: 1.2 })).toBe("recycle-now");
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe("readProcessRssBytes", () => {
|
|
255
|
-
test("reads this process's own rss", async () => {
|
|
256
|
-
const rssBytes = await AkanAppHost.readProcessRssBytes(process.pid);
|
|
257
|
-
if (rssBytes === null) throw new Error("expected to read this process's own rss");
|
|
258
|
-
// Loose bounds on purpose: the point is that it read a real number from the OS, not which number.
|
|
259
|
-
expect(rssBytes).toBeGreaterThan(1024 * 1024);
|
|
260
|
-
expect(rssBytes).toBeLessThan(64 * 1024 * 1024 * 1024);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Null rather than 0, because callers must treat an unreadable pid as "no new information" — a 0
|
|
264
|
-
// would read as "settled below the ceiling" and cancel a recycle that should happen.
|
|
265
|
-
test("returns null for a pid that does not exist", async () => {
|
|
266
|
-
expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
describe("dev idle suspend", () => {
|
|
272
|
-
const decide = (over: Partial<Parameters<typeof decideIdleSuspend>[0]> = {}) =>
|
|
273
|
-
decideIdleSuspend({
|
|
274
|
-
enabled: true,
|
|
275
|
-
suspended: false,
|
|
276
|
-
builderReady: true,
|
|
277
|
-
backendReady: true,
|
|
278
|
-
buildFailed: false,
|
|
279
|
-
restartPending: false,
|
|
280
|
-
msSinceWake: null,
|
|
281
|
-
...over,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("suspends an idle dev server whose builder and backend are both up", () => {
|
|
285
|
-
expect(decide()).toBe("suspend");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("keeps build capacity while anything is still in motion", () => {
|
|
289
|
-
expect(decide({ enabled: false })).toBe("disabled");
|
|
290
|
-
expect(decide({ suspended: true })).toBe("already-suspended");
|
|
291
|
-
expect(decide({ builderReady: false })).toBe("builder-not-ready");
|
|
292
|
-
expect(decide({ backendReady: false })).toBe("backend-not-ready");
|
|
293
|
-
expect(decide({ restartPending: true })).toBe("restart-pending");
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// A wake would boot straight back into the same compile error, and the developer is mid-fix anyway.
|
|
297
|
-
test("never suspends on a red build", () => {
|
|
298
|
-
expect(decide({ buildFailed: true })).toBe("build-failed");
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("enforces a minimum uptime after a wake so it cannot flap", () => {
|
|
302
|
-
expect(decide({ msSinceWake: 5_000 })).toBe("too-soon");
|
|
303
|
-
expect(decide({ msSinceWake: 31_000 })).toBe("suspend");
|
|
304
|
-
expect(decide({ msSinceWake: 5_000, minUptimeMs: 1_000 })).toBe("suspend");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("defaults to on and treats any non-positive value as off", () => {
|
|
308
|
-
expect(resolveIdleSuspendMs(undefined)).toBe(300_000);
|
|
309
|
-
expect(resolveIdleSuspendMs("")).toBe(300_000);
|
|
310
|
-
expect(resolveIdleSuspendMs("0")).toBeNull();
|
|
311
|
-
expect(resolveIdleSuspendMs("-1")).toBeNull();
|
|
312
|
-
expect(resolveIdleSuspendMs("not-a-number")).toBeNull();
|
|
313
|
-
expect(resolveIdleSuspendMs("1500")).toBe(1_500);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("blocks a suspend on a failure in any phase, not just the newest generation", () => {
|
|
317
|
-
const status = (phase: BuildPhase, ok: boolean): DevBuildStatus => ({ generation: 1, phase, ok, files: [] });
|
|
318
|
-
expect(hasAnyBuildFailure(new Map())).toBe(false);
|
|
319
|
-
expect(hasAnyBuildFailure(new Map([["scan", status("scan", true)]]))).toBe(false);
|
|
320
|
-
expect(
|
|
321
|
-
hasAnyBuildFailure(
|
|
322
|
-
new Map([
|
|
323
|
-
["scan", status("scan", true)],
|
|
324
|
-
["pages", status("pages", false)],
|
|
325
|
-
]),
|
|
326
|
-
),
|
|
327
|
-
).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("routes a config change made while suspended through the dev host restart", () => {
|
|
331
|
-
expect(shouldRefreshConfigOnIdleWake(null)).toBe(false);
|
|
332
|
-
expect(shouldRefreshConfigOnIdleWake({ files: ["/repo/apps/demo/ui/A.tsx"], kinds: new Set(["code"]) })).toBe(
|
|
333
|
-
false,
|
|
334
|
-
);
|
|
335
|
-
expect(
|
|
336
|
-
shouldRefreshConfigOnIdleWake({
|
|
337
|
-
files: ["/repo/apps/demo/akan.config.ts"],
|
|
338
|
-
kinds: new Set(["config", "code"]),
|
|
339
|
-
}),
|
|
340
|
-
).toBe(true);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
describe("recycled builder state announcements", () => {
|
|
345
|
-
const pages = (bundlePath: string): Extract<BuilderMessage, { type: "pages-updated" }> => ({
|
|
346
|
-
type: "pages-updated",
|
|
347
|
-
data: { bundlePath, buildId: 7, generation: 3, changedFiles: [], reason: "builder-recycle" },
|
|
348
|
-
});
|
|
349
|
-
const css = (cssUrl: string): Extract<BuilderMessage, { type: "css-updated" }> => ({
|
|
350
|
-
type: "css-updated",
|
|
351
|
-
data: {
|
|
352
|
-
cssAssets: { "": { cssUrl, cssRelPath: cssUrl.slice(1) } },
|
|
353
|
-
cssBase64ByUrl: { [cssUrl]: "" },
|
|
354
|
-
generation: 3,
|
|
355
|
-
changedFiles: [],
|
|
356
|
-
reason: "builder-recycle",
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Both identities are content hashes, so a recycle with no concurrent edit reproduces them exactly
|
|
361
|
-
// and must not reload the backend — that would refresh every browser on a memory recycle.
|
|
362
|
-
test("suppresses an unchanged pages announcement and relays a moved one", () => {
|
|
363
|
-
expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-abc.js"))).toBe(false);
|
|
364
|
-
expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-def.js"))).toBe(true);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test("suppresses an unchanged css announcement and relays a moved one", () => {
|
|
368
|
-
expect(shouldRelayRecycledFrontendState(css("/_akan/styles/root-abc.css"), css("/_akan/styles/root-abc.css"))).toBe(
|
|
369
|
-
false,
|
|
370
|
-
);
|
|
371
|
-
expect(shouldRelayRecycledFrontendState(css("/_akan/styles/root-abc.css"), css("/_akan/styles/root-def.css"))).toBe(
|
|
372
|
-
true,
|
|
373
|
-
);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("relays when the backend has no state of that kind yet", () => {
|
|
377
|
-
expect(shouldRelayRecycledFrontendState(undefined, pages("/a/pages-abc.js"))).toBe(true);
|
|
378
|
-
expect(shouldRelayRecycledFrontendState(css("/_akan/styles/root-abc.css"), pages("/a/pages-abc.js"))).toBe(true);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe("build status helpers", () => {
|
|
383
|
-
const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
|
|
384
|
-
generation,
|
|
385
|
-
phase,
|
|
386
|
-
ok,
|
|
387
|
-
files: [],
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("tracks recovery by phase without unrelated phases masking failures", () => {
|
|
391
|
-
const previousByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
392
|
-
["pages", status("pages", 10, false)],
|
|
393
|
-
["css", status("css", 11, true)],
|
|
394
|
-
]);
|
|
395
|
-
|
|
396
|
-
expect(shouldMarkBuildPhaseRecovered(previousByPhase, status("css", 12, true))).toBe(false);
|
|
397
|
-
expect(shouldMarkBuildPhaseRecovered(previousByPhase, status("pages", 12, true))).toBe(true);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test("creates backend build-status payloads for lifecycle test hooks", () => {
|
|
401
|
-
expect(
|
|
402
|
-
createBackendBuildStatus({
|
|
403
|
-
generation: 14,
|
|
404
|
-
ok: false,
|
|
405
|
-
files: ["/repo/libs/shared/common/foo.ts"],
|
|
406
|
-
message: "Backend exited unexpectedly",
|
|
407
|
-
}),
|
|
408
|
-
).toEqual({
|
|
409
|
-
generation: 14,
|
|
410
|
-
phase: "backend",
|
|
411
|
-
ok: false,
|
|
412
|
-
files: ["/repo/libs/shared/common/foo.ts"],
|
|
413
|
-
message: "Backend exited unexpectedly",
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("marks backend phase recovered independently", () => {
|
|
418
|
-
const previousByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
419
|
-
["backend", createBackendBuildStatus({ generation: 15, ok: false, message: "Backend exited" })],
|
|
420
|
-
["pages", status("pages", 16, false)],
|
|
421
|
-
]);
|
|
422
|
-
|
|
423
|
-
expect(shouldMarkBuildPhaseRecovered(previousByPhase, createBackendBuildStatus({ generation: 15, ok: true }))).toBe(
|
|
424
|
-
true,
|
|
425
|
-
);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("keeps backend failure and recovery statuses ordered for backend replay", () => {
|
|
429
|
-
const failed = createBackendBuildStatus({ generation: 15, ok: false, message: "Backend exited" });
|
|
430
|
-
const recovered = createBackendBuildStatus({ generation: 15, ok: true, message: "Backend ready" });
|
|
431
|
-
const latestByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([["backend", recovered]]);
|
|
432
|
-
|
|
433
|
-
expect(shouldQueueBuildStatusReplay(false, 0)).toBe(true);
|
|
434
|
-
expect(shouldQueueBuildStatusReplay(true, 1)).toBe(true);
|
|
435
|
-
expect(shouldQueueBuildStatusReplay(true, 0)).toBe(false);
|
|
436
|
-
expect(buildStatusReplaySequence([failed, recovered], latestByPhase).slice(0, 2)).toEqual([failed, recovered]);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
describe("BackendImportGraph", () => {
|
|
441
|
-
const tempRoots: string[] = [];
|
|
442
|
-
|
|
443
|
-
const makeGraph = async (files: Record<string, string>) => {
|
|
444
|
-
// Realpath, not the mkdtemp path: `Bun.resolveSync` returns real paths, and on macOS `/var/folders`
|
|
445
|
-
// is a symlink, so an unresolved root makes every resolved import look like it escapes the workspace.
|
|
446
|
-
const workspaceRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "akan-devkit-graph-")));
|
|
447
|
-
tempRoots.push(workspaceRoot);
|
|
448
|
-
const cwdPath = path.join(workspaceRoot, "apps/demo");
|
|
449
|
-
for (const [rel, source] of Object.entries(files)) {
|
|
450
|
-
const filePath = path.join(cwdPath, rel);
|
|
451
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
452
|
-
await writeFile(filePath, source);
|
|
453
|
-
}
|
|
454
|
-
const app = { cwdPath, workspace: { workspaceRoot } } as unknown as App;
|
|
455
|
-
return { graph: new BackendImportGraph(app, new Logger("test")), cwdPath };
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
afterEach(async () => {
|
|
459
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
test("walks the backend entrypoints' import graph", async () => {
|
|
463
|
-
const { graph, cwdPath } = await makeGraph({
|
|
464
|
-
"main.ts": 'import "./server";\n',
|
|
465
|
-
"server.ts": 'import { handler } from "./lib/handler";\nexport default handler;\n',
|
|
466
|
-
"lib/handler.ts": "export const handler = () => null;\n",
|
|
467
|
-
"lib/unreachable.ts": "export const nope = 1;\n",
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
expect(await graph.refresh()).toBe(true);
|
|
471
|
-
expect(graph.has(path.join(cwdPath, "lib/handler.ts"))).toBe(true);
|
|
472
|
-
expect(graph.has(path.join(cwdPath, "lib/unreachable.ts"))).toBe(false);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test("picks up an import added to an already-scanned file", async () => {
|
|
476
|
-
const { graph, cwdPath } = await makeGraph({
|
|
477
|
-
"main.ts": 'import "./server";\n',
|
|
478
|
-
"server.ts": "export default 1;\n",
|
|
479
|
-
"lib/added.ts": "export const added = 1;\n",
|
|
480
|
-
});
|
|
481
|
-
await graph.refresh();
|
|
482
|
-
expect(graph.has(path.join(cwdPath, "lib/added.ts"))).toBe(false);
|
|
483
|
-
|
|
484
|
-
// The scan cache is keyed on (mtimeMs, size), so the rewrite must invalidate it.
|
|
485
|
-
await writeFile(path.join(cwdPath, "server.ts"), 'import "./lib/added";\nexport default 1;\n');
|
|
486
|
-
|
|
487
|
-
await graph.refresh();
|
|
488
|
-
expect(graph.has(path.join(cwdPath, "lib/added.ts"))).toBe(true);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test("drops a file that left the graph", async () => {
|
|
492
|
-
const { graph, cwdPath } = await makeGraph({
|
|
493
|
-
"main.ts": 'import "./server";\n',
|
|
494
|
-
"server.ts": 'import "./lib/leaving";\nexport default 1;\n',
|
|
495
|
-
"lib/leaving.ts": "export const leaving = 1;\n",
|
|
496
|
-
});
|
|
497
|
-
await graph.refresh();
|
|
498
|
-
expect(graph.has(path.join(cwdPath, "lib/leaving.ts"))).toBe(true);
|
|
499
|
-
|
|
500
|
-
await writeFile(path.join(cwdPath, "server.ts"), "export default 1;\n");
|
|
501
|
-
await graph.refresh();
|
|
502
|
-
expect(graph.has(path.join(cwdPath, "lib/leaving.ts"))).toBe(false);
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
test("reports which backend files moved while nobody was watching", async () => {
|
|
506
|
-
const { graph, cwdPath } = await makeGraph({
|
|
507
|
-
"main.ts": 'import "./server";\n',
|
|
508
|
-
"server.ts": 'import "./lib/handler";\nexport default 1;\n',
|
|
509
|
-
"lib/handler.ts": "export const handler = () => null;\n",
|
|
510
|
-
});
|
|
511
|
-
await graph.refresh();
|
|
512
|
-
const before = await graph.fingerprint();
|
|
513
|
-
|
|
514
|
-
// The builder is gone here, so no watcher event exists for this save — which is the whole reason
|
|
515
|
-
// the stamps are taken. `mtimeMs` has a coarse clock on Linux, so the size has to move too.
|
|
516
|
-
await writeFile(path.join(cwdPath, "lib/handler.ts"), "export const handler = () => 'changed';\n");
|
|
517
|
-
|
|
518
|
-
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test("says nothing when the tree is untouched, and names a deleted file", async () => {
|
|
522
|
-
const { graph, cwdPath } = await makeGraph({
|
|
523
|
-
"main.ts": 'import "./server";\n',
|
|
524
|
-
"server.ts": 'import "./lib/handler";\nexport default 1;\n',
|
|
525
|
-
"lib/handler.ts": "export const handler = () => null;\n",
|
|
526
|
-
});
|
|
527
|
-
await graph.refresh();
|
|
528
|
-
const before = await graph.fingerprint();
|
|
529
|
-
// A recycle with no edit in it is the common case, and it must not cost a backend restart.
|
|
530
|
-
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([]);
|
|
531
|
-
|
|
532
|
-
await rm(path.join(cwdPath, "lib/handler.ts"));
|
|
533
|
-
// Deleted counts as changed: the backend is still running what used to be there.
|
|
534
|
-
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test("keeps the previous graph when a refresh finds no entrypoints", async () => {
|
|
538
|
-
const { graph, cwdPath } = await makeGraph({
|
|
539
|
-
"main.ts": 'import "./lib/kept";\n',
|
|
540
|
-
"lib/kept.ts": "export const kept = 1;\n",
|
|
541
|
-
});
|
|
542
|
-
await graph.refresh();
|
|
543
|
-
expect(graph.ready).toBe(true);
|
|
544
|
-
|
|
545
|
-
await rm(path.join(cwdPath, "main.ts"));
|
|
546
|
-
await graph.refresh();
|
|
547
|
-
// An empty scan is not a failure, so the graph legitimately empties out.
|
|
548
|
-
expect(graph.has(path.join(cwdPath, "lib/kept.ts"))).toBe(false);
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
describe("legacy backend graph fallback", () => {
|
|
553
|
-
const root = "/repo";
|
|
554
|
-
|
|
555
|
-
test("treats server and shared path roles as backend restart candidates", () => {
|
|
556
|
-
expect(isLegacyBackendFallbackFile(`${root}/libs/shared/srvkit/foo.ts`, root)).toBe(true);
|
|
557
|
-
expect(isLegacyBackendFallbackFile(`${root}/libs/shared/common/foo.ts`, root)).toBe(true);
|
|
558
|
-
expect(isLegacyBackendFallbackFile(`${root}/libs/shared/lib/admin/admin.signal.ts`, root)).toBe(true);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test("does not restart backend for client-only path roles", () => {
|
|
562
|
-
expect(isLegacyBackendFallbackFile(`${root}/libs/shared/ui/Foo.tsx`, root)).toBe(false);
|
|
563
|
-
expect(isLegacyBackendFallbackFile(`${root}/apps/akan/page/_index.tsx`, root)).toBe(false);
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
describe("backend recovery abandonment", () => {
|
|
568
|
-
test("keeps retrying below the attempt ceiling and abandons at it", () => {
|
|
569
|
-
expect(shouldAbandonBackendRecovery(0)).toBe(false);
|
|
570
|
-
expect(shouldAbandonBackendRecovery(4)).toBe(false);
|
|
571
|
-
expect(shouldAbandonBackendRecovery(5)).toBe(true);
|
|
572
|
-
expect(shouldAbandonBackendRecovery(9)).toBe(true);
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test("honors a custom attempt ceiling", () => {
|
|
576
|
-
expect(shouldAbandonBackendRecovery(2, 3)).toBe(false);
|
|
577
|
-
expect(shouldAbandonBackendRecovery(3, 3)).toBe(true);
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
describe("hasBuildFailureForGeneration", () => {
|
|
582
|
-
const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
|
|
583
|
-
generation,
|
|
584
|
-
phase,
|
|
585
|
-
ok,
|
|
586
|
-
files: [],
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test("detects a failing phase recorded for the same generation", () => {
|
|
590
|
-
const statusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
591
|
-
["csr", status("csr", 3, false)],
|
|
592
|
-
["barrel", status("barrel", 3, true)],
|
|
593
|
-
]);
|
|
594
|
-
expect(hasBuildFailureForGeneration(statusByPhase, 3)).toBe(true);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
test("ignores stale failures from earlier generations", () => {
|
|
598
|
-
const statusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
599
|
-
["csr", status("csr", 3, false)],
|
|
600
|
-
["barrel", status("barrel", 4, true)],
|
|
601
|
-
]);
|
|
602
|
-
expect(hasBuildFailureForGeneration(statusByPhase, 4)).toBe(false);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("treats unknown generations and green boards as healthy", () => {
|
|
606
|
-
const statusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([["csr", status("csr", 3, false)]]);
|
|
607
|
-
expect(hasBuildFailureForGeneration(statusByPhase, undefined)).toBe(false);
|
|
608
|
-
expect(hasBuildFailureForGeneration(new Map(), 3)).toBe(false);
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
describe("mergeInvalidateMessages", () => {
|
|
613
|
-
const invalidate = (
|
|
614
|
-
generation: number,
|
|
615
|
-
files: string[],
|
|
616
|
-
actions: DevChangeAction[],
|
|
617
|
-
): Extract<BuilderMessage, { type: "invalidate" }> => ({
|
|
618
|
-
type: "invalidate",
|
|
619
|
-
kinds: ["code"],
|
|
620
|
-
files,
|
|
621
|
-
generation,
|
|
622
|
-
devPlan: {
|
|
623
|
-
generation,
|
|
624
|
-
files,
|
|
625
|
-
generatedFiles: [],
|
|
626
|
-
roles: ["shared"],
|
|
627
|
-
actions,
|
|
628
|
-
reasonByFile: { [files[0] ?? "/repo/a.ts"]: ["shared-path"] },
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
test("unions files, kinds, and actions while keeping the latest generation", () => {
|
|
633
|
-
const merged = mergeInvalidateMessages(
|
|
634
|
-
invalidate(3, ["/repo/b.ts", "/repo/a.ts"], ["restart-builder"]),
|
|
635
|
-
invalidate(5, ["/repo/c.ts"], ["rebuild-client"]),
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
expect(merged.generation).toBe(5);
|
|
639
|
-
expect(merged.files).toEqual(["/repo/a.ts", "/repo/b.ts", "/repo/c.ts"]);
|
|
640
|
-
expect(merged.devPlan?.generation).toBe(5);
|
|
641
|
-
expect(merged.devPlan?.actions).toEqual(["rebuild-client", "restart-builder"]);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("keeps the surviving devPlan when only one side carries one", () => {
|
|
645
|
-
const withPlan = invalidate(3, ["/repo/a.ts"], ["restart-builder"]);
|
|
646
|
-
const withoutPlan: Extract<BuilderMessage, { type: "invalidate" }> = {
|
|
647
|
-
type: "invalidate",
|
|
648
|
-
kinds: ["css"],
|
|
649
|
-
files: ["/repo/style.css"],
|
|
650
|
-
generation: 4,
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
const merged = mergeInvalidateMessages(withPlan, withoutPlan);
|
|
654
|
-
expect(merged.devPlan?.actions).toEqual(["restart-builder"]);
|
|
655
|
-
expect(merged.kinds).toEqual(["code", "css"]);
|
|
656
|
-
expect(merged.generation).toBe(4);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
test("merges per-file reasons without duplicating entries", () => {
|
|
660
|
-
const current = invalidate(3, ["/repo/a.ts"], ["restart-builder"]);
|
|
661
|
-
const next = invalidate(4, ["/repo/a.ts"], ["restart-builder"]);
|
|
662
|
-
const nextPlan = next.devPlan;
|
|
663
|
-
if (!nextPlan) throw new Error("devPlan expected");
|
|
664
|
-
nextPlan.reasonByFile["/repo/a.ts"] = ["runtime-metadata", "shared-path"];
|
|
665
|
-
|
|
666
|
-
const merged = mergeInvalidateMessages(current, next);
|
|
667
|
-
expect(merged.devPlan?.reasonByFile["/repo/a.ts"]).toEqual(["runtime-metadata", "shared-path"]);
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
describe("normalizeBackendReportedGeneration", () => {
|
|
672
|
-
test("drops the gateway's unknown-generation sentinel", () => {
|
|
673
|
-
expect(normalizeBackendReportedGeneration(-1)).toBeUndefined();
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
test("keeps real generations including zero", () => {
|
|
677
|
-
expect(normalizeBackendReportedGeneration(0)).toBe(0);
|
|
678
|
-
expect(normalizeBackendReportedGeneration(7)).toBe(7);
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { AkanAppHost } from "./akanApp.host";
|
|
3
|
+
|
|
4
|
+
describe("readProcessRssBytes", () => {
|
|
5
|
+
test("reads this process's own rss", async () => {
|
|
6
|
+
const rssBytes = await AkanAppHost.readProcessRssBytes(process.pid);
|
|
7
|
+
if (rssBytes === null) throw new Error("expected to read this process's own rss");
|
|
8
|
+
// Loose bounds on purpose: the point is that it read a real number from the OS, not which number.
|
|
9
|
+
expect(rssBytes).toBeGreaterThan(1024 * 1024);
|
|
10
|
+
expect(rssBytes).toBeLessThan(64 * 1024 * 1024 * 1024);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Null rather than 0, because callers must treat an unreadable pid as "no new information" — a 0
|
|
14
|
+
// would read as "settled below the ceiling" and cancel a recycle that should happen.
|
|
15
|
+
test("returns null for a pid that does not exist", async () => {
|
|
16
|
+
expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
|
|
679
17
|
});
|
|
680
18
|
});
|