@akanjs/devkit 3.0.0-beta.9 → 3.0.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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -19,8 +19,6 @@ export interface DevStabilityHost {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface DevStabilityHmrProbe {
|
|
22
|
-
/** The live socket — a reconnect replaces it, so read it rather than holding on to one. */
|
|
23
|
-
readonly ws: WebSocket;
|
|
24
22
|
readonly reconnects: number;
|
|
25
23
|
messages: unknown[];
|
|
26
24
|
mark(): number;
|
|
@@ -43,7 +41,6 @@ export class DevStabilityHarness {
|
|
|
43
41
|
#portAllocation: Promise<{ port: number; offset: number }> | null = null;
|
|
44
42
|
#boundPort: number | null = null;
|
|
45
43
|
#host: DevStabilityHost | null = null;
|
|
46
|
-
/** What the last HTTP poll saw, so a timeout can say which port answered and with what. */
|
|
47
44
|
#lastHttpProbe: { port: number; body: string | null } = { port: 0, body: null };
|
|
48
45
|
|
|
49
46
|
constructor({
|
|
@@ -63,47 +60,31 @@ export class DevStabilityHarness {
|
|
|
63
60
|
|
|
64
61
|
async createFixture(): Promise<DevStabilityFixture> {
|
|
65
62
|
await rm(this.appDir, { recursive: true, force: true });
|
|
66
|
-
await Promise.all(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
mkdir(path.join(this.appDir, "env"), { recursive: true }),
|
|
74
|
-
mkdir(path.join(this.appDir, "public"), { recursive: true }),
|
|
75
|
-
]);
|
|
76
|
-
await Promise.all([
|
|
77
|
-
this.writeFile(
|
|
78
|
-
"main.ts",
|
|
79
|
-
`import { AkanApp } from "akanjs/server";
|
|
63
|
+
await Promise.all(
|
|
64
|
+
["page", "common", "srvkit", "ui", "webkit", "lib", "env", "public"].map((dir) =>
|
|
65
|
+
mkdir(path.join(this.appDir, dir), { recursive: true }),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
const files: { [relativePath: string]: string } = {
|
|
69
|
+
"main.ts": `import { AkanApp } from "akanjs/server";
|
|
80
70
|
|
|
81
71
|
const run = async () => {
|
|
82
72
|
await new AkanApp("./server").start();
|
|
83
73
|
};
|
|
84
74
|
void run();
|
|
85
75
|
`,
|
|
86
|
-
|
|
87
|
-
this.writeFile(
|
|
88
|
-
"akan.config.ts",
|
|
89
|
-
`import type { AppConfig } from "akanjs";
|
|
76
|
+
"akan.config.ts": `import type { AppConfig } from "akanjs";
|
|
90
77
|
|
|
91
78
|
const config: AppConfig = {};
|
|
92
79
|
export default config;
|
|
93
80
|
`,
|
|
94
|
-
|
|
95
|
-
this.writeFile(
|
|
96
|
-
"package.json",
|
|
97
|
-
`{
|
|
81
|
+
"package.json": `{
|
|
98
82
|
"type": "module",
|
|
99
83
|
"name": "${this.appName}",
|
|
100
84
|
"version": "0.0.1"
|
|
101
85
|
}
|
|
102
86
|
`,
|
|
103
|
-
|
|
104
|
-
this.writeFile(
|
|
105
|
-
"tsconfig.json",
|
|
106
|
-
`{
|
|
87
|
+
"tsconfig.json": `{
|
|
107
88
|
"extends": "../../tsconfig.json",
|
|
108
89
|
"compilerOptions": {
|
|
109
90
|
"allowJs": true,
|
|
@@ -115,33 +96,18 @@ export default config;
|
|
|
115
96
|
"include": ["./**/*.ts", "./**/*.tsx"]
|
|
116
97
|
}
|
|
117
98
|
`,
|
|
118
|
-
|
|
119
|
-
this.writeFile(
|
|
120
|
-
"env/env.client.ts",
|
|
121
|
-
`export const env = {} as const;
|
|
99
|
+
"env/env.client.ts": `export const env = {} as const;
|
|
122
100
|
`,
|
|
123
|
-
|
|
124
|
-
this.writeFile(
|
|
125
|
-
"env/env.server.ts",
|
|
126
|
-
`export const env = {} as const;
|
|
101
|
+
"env/env.server.ts": `export const env = {} as const;
|
|
127
102
|
`,
|
|
128
|
-
|
|
129
|
-
this.writeFile(
|
|
130
|
-
"env/env.server.testing.ts",
|
|
131
|
-
`export { env } from "./env.server";
|
|
103
|
+
"env/env.server.testing.ts": `export { env } from "./env.server";
|
|
132
104
|
`,
|
|
133
|
-
|
|
134
|
-
this.writeFile(
|
|
135
|
-
"lib/option.ts",
|
|
136
|
-
`import { AkanOption } from "akanjs/server";
|
|
105
|
+
"lib/option.ts": `import { AkanOption } from "akanjs/server";
|
|
137
106
|
|
|
138
107
|
export type ModulesOptions = Record<string, never>;
|
|
139
108
|
export const option = new AkanOption<ModulesOptions>();
|
|
140
109
|
`,
|
|
141
|
-
|
|
142
|
-
this.writeFile(
|
|
143
|
-
"server.ts",
|
|
144
|
-
`import { AkanServer, AkanLib } from "akanjs/server";
|
|
110
|
+
"server.ts": `import { AkanServer, AkanLib } from "akanjs/server";
|
|
145
111
|
import { backendMarker } from "./srvkit/backendMarker";
|
|
146
112
|
|
|
147
113
|
void backendMarker;
|
|
@@ -155,20 +121,14 @@ export const server = new AkanServer("${this.appName}", {
|
|
|
155
121
|
serveDomain: "localhost",
|
|
156
122
|
} as never, undefined, lib);
|
|
157
123
|
`,
|
|
158
|
-
|
|
159
|
-
this.writeFile(
|
|
160
|
-
"page/_layout.tsx",
|
|
161
|
-
`import "./styles.css";
|
|
124
|
+
"page/_layout.tsx": `import "./styles.css";
|
|
162
125
|
import { layout } from "akanjs/client";
|
|
163
126
|
|
|
164
127
|
export default layout().render(({ children }) => {
|
|
165
128
|
return <>{children}</>;
|
|
166
129
|
});
|
|
167
130
|
`,
|
|
168
|
-
|
|
169
|
-
this.writeFile(
|
|
170
|
-
"page/_index.tsx",
|
|
171
|
-
`import { page } from "akanjs/client";
|
|
131
|
+
"page/_index.tsx": `import { page } from "akanjs/client";
|
|
172
132
|
import { marker } from "../common/marker";
|
|
173
133
|
import { ClientMarker } from "../ui/ClientMarker";
|
|
174
134
|
|
|
@@ -182,13 +142,8 @@ export default page().render(() => {
|
|
|
182
142
|
);
|
|
183
143
|
});
|
|
184
144
|
`,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// its choosing. Route clients are built on demand and cached, so on a one-route fixture the first
|
|
188
|
-
// request of a test is the only one that reaches the builder at all.
|
|
189
|
-
this.writeFile(
|
|
190
|
-
"page/second/_index.tsx",
|
|
191
|
-
`import { page } from "akanjs/client";
|
|
145
|
+
// Route clients are built on demand and cached, so only a never-requested route reaches the builder.
|
|
146
|
+
"page/second/_index.tsx": `import { page } from "akanjs/client";
|
|
192
147
|
import { ClientMarker } from "../../ui/ClientMarker";
|
|
193
148
|
|
|
194
149
|
export default page().render(() => {
|
|
@@ -200,34 +155,19 @@ export default page().render(() => {
|
|
|
200
155
|
);
|
|
201
156
|
});
|
|
202
157
|
`,
|
|
203
|
-
|
|
204
|
-
this.writeFile(
|
|
205
|
-
"page/styles.css",
|
|
206
|
-
`main {
|
|
158
|
+
"page/styles.css": `main {
|
|
207
159
|
color: black;
|
|
208
160
|
}
|
|
209
161
|
`,
|
|
210
|
-
|
|
211
|
-
this.writeFile(
|
|
212
|
-
"common/marker.ts",
|
|
213
|
-
`export const marker = "initial-shared-marker";
|
|
162
|
+
"common/marker.ts": `export const marker = "initial-shared-marker";
|
|
214
163
|
`,
|
|
215
|
-
|
|
216
|
-
this.writeFile(
|
|
217
|
-
"srvkit/backendMarker.ts",
|
|
218
|
-
`export const backendMarker = "initial-backend-marker";
|
|
164
|
+
"srvkit/backendMarker.ts": `export const backendMarker = "initial-backend-marker";
|
|
219
165
|
`,
|
|
220
|
-
|
|
221
|
-
this.writeFile(
|
|
222
|
-
"lib/_fixture/fixture.service.ts",
|
|
223
|
-
`import { serve } from "akanjs/service";
|
|
166
|
+
"lib/_fixture/fixture.service.ts": `import { serve } from "akanjs/service";
|
|
224
167
|
|
|
225
168
|
export class FixtureService extends serve("fixture" as const, { serverMode: "batch" }, () => ({})) {}
|
|
226
169
|
`,
|
|
227
|
-
|
|
228
|
-
this.writeFile(
|
|
229
|
-
"lib/_fixture/fixture.signal.ts",
|
|
230
|
-
`import { endpoint, internal } from "akanjs/signal";
|
|
170
|
+
"lib/_fixture/fixture.signal.ts": `import { endpoint, internal } from "akanjs/signal";
|
|
231
171
|
|
|
232
172
|
import * as srv from "../srv";
|
|
233
173
|
|
|
@@ -235,10 +175,7 @@ export class FixtureInternal extends internal(srv.fixture, () => ({})) {}
|
|
|
235
175
|
|
|
236
176
|
export class FixtureEndpoint extends endpoint(srv.fixture, () => ({})) {}
|
|
237
177
|
`,
|
|
238
|
-
|
|
239
|
-
this.writeFile(
|
|
240
|
-
"lib/_fixture/fixture.dictionary.ts",
|
|
241
|
-
`import { serviceDictionary } from "akanjs/dictionary";
|
|
178
|
+
"lib/_fixture/fixture.dictionary.ts": `import { serviceDictionary } from "akanjs/dictionary";
|
|
242
179
|
|
|
243
180
|
import type { FixtureEndpoint } from "./fixture.signal";
|
|
244
181
|
|
|
@@ -249,32 +186,19 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
249
186
|
removeMe: ["Remove Me", "삭제 예정"],
|
|
250
187
|
});
|
|
251
188
|
`,
|
|
252
|
-
)
|
|
253
|
-
this.writeFile(
|
|
254
|
-
"ui/ClientMarker.tsx",
|
|
255
|
-
`export function ClientMarker() {
|
|
189
|
+
"ui/ClientMarker.tsx": `export function ClientMarker() {
|
|
256
190
|
return <p data-testid="client-marker">initial-client-marker</p>;
|
|
257
191
|
}
|
|
258
192
|
`,
|
|
259
|
-
)
|
|
260
|
-
this.writeFile(
|
|
261
|
-
"webkit/useMarker.ts",
|
|
262
|
-
`export const useMarker = () => "initial-webkit-marker";
|
|
193
|
+
"webkit/useMarker.ts": `export const useMarker = () => "initial-webkit-marker";
|
|
263
194
|
`,
|
|
264
|
-
|
|
265
|
-
]);
|
|
195
|
+
};
|
|
196
|
+
await Promise.all(Object.entries(files).map(([relativePath, contents]) => this.writeFile(relativePath, contents)));
|
|
266
197
|
const port = await this.resolvePort();
|
|
267
198
|
return { appName: this.appName, appDir: this.appDir, workspaceRoot: this.workspaceRoot, port };
|
|
268
199
|
}
|
|
269
200
|
|
|
270
|
-
|
|
271
|
-
* Stop the dev server and delete the fixture, within a budget that cannot outlive the hook awaiting it.
|
|
272
|
-
*
|
|
273
|
-
* Overrunning the `afterEach` budget costs more than one red test. Bun fails the test that had *already
|
|
274
|
-
* passed*, and the fixture is dropped from the cleanup list either way — so its dev host keeps running,
|
|
275
|
-
* holding ports and shifting the app index, and the tests after it fail too. That cascade is what turned
|
|
276
|
-
* one slow cleanup into three failures in a parallel shard.
|
|
277
|
-
*/
|
|
201
|
+
// Bounded: an overrun `afterEach` fails the already-passed test and leaves this host running into the next ones.
|
|
278
202
|
async cleanup(): Promise<void> {
|
|
279
203
|
const startedAt = Date.now();
|
|
280
204
|
const finished = await Promise.race([
|
|
@@ -286,8 +210,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
286
210
|
console.warn(`[harness] ${this.appName} cleanup took ${Date.now() - startedAt}ms`);
|
|
287
211
|
return;
|
|
288
212
|
}
|
|
289
|
-
//
|
|
290
|
-
// `ps` and cannot itself hang, which is what the orderly path just demonstrated it can do.
|
|
213
|
+
// Killing the tracked child directly needs no `ps`, so it cannot hang the way the orderly path just did.
|
|
291
214
|
this.#host?.proc.kill("SIGKILL");
|
|
292
215
|
this.#host = null;
|
|
293
216
|
console.warn(
|
|
@@ -304,17 +227,10 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
304
227
|
}
|
|
305
228
|
|
|
306
229
|
static readonly #slowCleanupMs = 10_000;
|
|
307
|
-
|
|
308
|
-
* which phase stalled — rather than by Bun, which only knows that a hook did not return. */
|
|
230
|
+
// Inside the suite's 60s hook budget, so the harness names the stalled phase before Bun's bare hook timeout.
|
|
309
231
|
static readonly #cleanupBudgetMs = 30_000;
|
|
310
232
|
|
|
311
|
-
|
|
312
|
-
* Names a phase that is still running, rather than only reporting one that finished.
|
|
313
|
-
*
|
|
314
|
-
* A cleanup that never returns is reported by Bun as a bare `a beforeEach/afterEach hook timed out`
|
|
315
|
-
* against the test that had already passed, with nothing at all about where it hung — so any
|
|
316
|
-
* end-of-phase timing is exactly the information a hang destroys.
|
|
317
|
-
*/
|
|
233
|
+
// Bun reports a hung hook with no location, so warn while a phase is still running, not after it ends.
|
|
318
234
|
static async #watched<T>(label: string, work: () => Promise<T>): Promise<T> {
|
|
319
235
|
const timer = setTimeout(
|
|
320
236
|
() => console.warn(`[harness] ${label} still running after ${DevStabilityHarness.#slowCleanupMs}ms`),
|
|
@@ -350,17 +266,11 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
350
266
|
env: {
|
|
351
267
|
...process.env,
|
|
352
268
|
AKAN_VERBOSE: "1",
|
|
353
|
-
// The dev-plan/hmr assertions match verbose-level
|
|
354
|
-
// stream only carries info/warn/error and those waits time out with empty tails.
|
|
269
|
+
// The dev-plan/hmr assertions match verbose-level lines.
|
|
355
270
|
AKAN_PUBLIC_LOG_LEVEL: "verbose",
|
|
356
271
|
NODE_NO_WARNINGS: "1",
|
|
357
272
|
PORT_OFFSET: String(offset),
|
|
358
|
-
// Pinned
|
|
359
|
-
// listing, and a parallel run adds and removes fixtures constantly — so the index moved between the
|
|
360
|
-
// allocation here and the host reading it, and again on every restart. Tests that merely *wait* on a
|
|
361
|
-
// port recovered by adopting the logged one, but a test that has to reserve a port before boot (the
|
|
362
|
-
// occupied-ws-port test blocks `port + 10_000`) had no way to be right, and spent 60s waiting for a
|
|
363
|
-
// fallback that could not happen because the gateway had gone to a different port entirely.
|
|
273
|
+
// Pinned: `getDevPort()` derives from the fixture's index in `apps/`, which parallel runs keep moving.
|
|
364
274
|
AKAN_DEV_PORT: String(port),
|
|
365
275
|
...env,
|
|
366
276
|
},
|
|
@@ -387,19 +297,15 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
387
297
|
const host: DevStabilityHost = {
|
|
388
298
|
proc,
|
|
389
299
|
logs,
|
|
390
|
-
markLog: () =>
|
|
300
|
+
markLog: () => logs.join("").length,
|
|
391
301
|
waitForLog: (pattern, waitMs) =>
|
|
392
|
-
DevStabilityHarness.#timed(`waitForLog ${pattern}`, () =>
|
|
302
|
+
DevStabilityHarness.#timed(`waitForLog ${pattern}`, () => waitForLogSince(logs, 0, pattern, waitMs)),
|
|
393
303
|
waitForLogSince: (mark, pattern, waitMs) =>
|
|
394
304
|
DevStabilityHarness.#timed(`waitForLogSince ${pattern}`, () => waitForLogSince(logs, mark, pattern, waitMs)),
|
|
395
305
|
stop: async () => {
|
|
396
306
|
await DevStabilityHarness.#dumpLogs(this.appName, logs);
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// interfere with later tests. Collect the descendants first, then signal all of them.
|
|
400
|
-
//
|
|
401
|
-
// The tracked child is always signalled directly as well, because `descendantPids` comes back empty
|
|
402
|
-
// when the `ps` behind it times out — and that path must still take the shell down.
|
|
307
|
+
// Under `bash -lc`, killing `proc` only kills the shell, so every descendant is signalled too; `proc`
|
|
308
|
+
// itself is signalled directly because `descendantPids` is empty when `ps` times out.
|
|
403
309
|
const pids = await DevStabilityHarness.#watched("descendants", () =>
|
|
404
310
|
DevStabilityHarness.descendantPids(proc.pid),
|
|
405
311
|
);
|
|
@@ -414,8 +320,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
414
320
|
DevStabilityHarness.#signalPids(survivors, "SIGKILL");
|
|
415
321
|
DevStabilityHarness.#signalPids(pids, "SIGKILL");
|
|
416
322
|
proc.kill("SIGKILL");
|
|
417
|
-
// Signalling is not reaping
|
|
418
|
-
// into `.akan/artifact` while the caller's `rm -rf` walks the same tree.
|
|
323
|
+
// Signalling is not reaping: a live build worker would keep writing into the tree the caller deletes next.
|
|
419
324
|
const signalled = [...new Set([...pids, ...survivors])];
|
|
420
325
|
const gone = await DevStabilityHarness.#watched("await gone", () =>
|
|
421
326
|
DevStabilityHarness.#waitForPidsGone(signalled, 15_000),
|
|
@@ -439,13 +344,6 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
439
344
|
this.#host = null;
|
|
440
345
|
}
|
|
441
346
|
|
|
442
|
-
/**
|
|
443
|
-
* Write a host's whole log to `AKAN_DEV_STABILITY_LOG_DIR` when it is set.
|
|
444
|
-
*
|
|
445
|
-
* A failing wait prints only a tail, and a *passing* run prints nothing — so a stall that stays inside
|
|
446
|
-
* the timeout budget is invisible, which is exactly the state a slow-but-green suite hides. Off unless
|
|
447
|
-
* the variable is set, since the suite otherwise produces megabytes per round.
|
|
448
|
-
*/
|
|
449
347
|
static async #dumpLogs(appName: string, logs: string[]): Promise<void> {
|
|
450
348
|
const dir = process.env.AKAN_DEV_STABILITY_LOG_DIR;
|
|
451
349
|
if (!dir) return;
|
|
@@ -459,49 +357,22 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
459
357
|
await writeFile(target, contents);
|
|
460
358
|
}
|
|
461
359
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
*
|
|
465
|
-
* Bun's recursive `fs.watch` reports about one path per coalescing window and discards the rest, so an
|
|
466
|
-
* edit landing in the same window as a build's write burst used to be dropped entirely — no rebuild, no
|
|
467
|
-
* HMR, no error. In a 3-way parallel run that failed the same test in all three shards, with 60 seconds
|
|
468
|
-
* of *completely empty* log output after the edit.
|
|
469
|
-
*
|
|
470
|
-
* `HmrWatcher` now resolves changes against a `SourceMtimeIndex` instead of trusting those payloads, so
|
|
471
|
-
* this no longer settles before editing: edits deliberately land right after the previous build's burst,
|
|
472
|
-
* which is exactly the case that used to be lost. `retried` staying 0 is therefore evidence the fix
|
|
473
|
-
* holds, where before it only meant the settle had dodged the window.
|
|
474
|
-
*
|
|
475
|
-
* The retry stays as the regression detector. What it waits for is evidence the watcher saw the edit,
|
|
476
|
-
* not the outcome the caller cares about: evidence arrives within a debounce, while an outcome can
|
|
477
|
-
* legitimately take far longer than any retry budget — so callers keep their own assertions, patterns
|
|
478
|
-
* and timeouts exactly as they were. An edit the dev server genuinely fails to act on exhausts every
|
|
479
|
-
* attempt and fails the test rather than being retried into a pass.
|
|
480
|
-
*/
|
|
360
|
+
// Bun's recursive `fs.watch` can drop an edit landing in a build's write burst, so re-apply it until the dev
|
|
361
|
+
// server logs evidence of seeing it (a regression detector: `retried` should stay 0); callers assert outcomes.
|
|
481
362
|
async editUntilSeen(
|
|
482
363
|
host: DevStabilityHost,
|
|
483
364
|
mutate: (attempt: number) => Promise<void>,
|
|
484
365
|
{
|
|
485
366
|
evidence = DevStabilityHarness.#editEvidence,
|
|
486
367
|
attempts = 3,
|
|
487
|
-
// Generous
|
|
488
|
-
// discrimination — while a budget tight enough to mistake a loaded machine for a dropped event makes
|
|
489
|
-
// the retry actively harmful, since re-applying an edit is not always cheap (a config rewrite costs a
|
|
490
|
-
// whole dev-host restart, and four of those overran a 120s test).
|
|
368
|
+
// Generous: a dropped event never arrives late, while a needless retry can cost a whole dev-host restart.
|
|
491
369
|
evidenceTimeoutMs = 20_000,
|
|
492
|
-
settleMs = 0,
|
|
493
|
-
retryDelayMs = 750,
|
|
494
370
|
}: {
|
|
495
371
|
evidence?: RegExp;
|
|
496
372
|
attempts?: number;
|
|
497
373
|
evidenceTimeoutMs?: number;
|
|
498
|
-
settleMs?: number;
|
|
499
|
-
retryDelayMs?: number;
|
|
500
374
|
} = {},
|
|
501
375
|
): Promise<{ mark: number; attempts: number; evidence: RegExpMatchArray }> {
|
|
502
|
-
// Kept as an escape hatch for a caller that needs spacing for its own reasons; the watcher no longer
|
|
503
|
-
// needs it, so the default is 0 and edits land inside the burst window on purpose.
|
|
504
|
-
if (settleMs > 0) await wait(settleMs);
|
|
505
376
|
for (let attempt = 1; ; attempt++) {
|
|
506
377
|
const mark = host.markLog();
|
|
507
378
|
await mutate(attempt);
|
|
@@ -515,12 +386,10 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
515
386
|
throw new Error(
|
|
516
387
|
`Dev server never reacted to ${attempts} edit(s) of ${this.appName}: waited ${evidenceTimeoutMs}ms each for ${evidence}`,
|
|
517
388
|
);
|
|
518
|
-
await wait(
|
|
389
|
+
await wait(750);
|
|
519
390
|
}
|
|
520
391
|
}
|
|
521
392
|
|
|
522
|
-
/** Every classified change logs a dev plan before anything acts on it, and an idle-suspended host logs a
|
|
523
|
-
* wake first, so either line proves the watcher saw the edit. */
|
|
524
393
|
static readonly #editEvidence = /\[dev-plan\] generation=\d+|\[idle-suspend\] waking/;
|
|
525
394
|
static #observedEdits = 0;
|
|
526
395
|
static #retriedEdits = 0;
|
|
@@ -531,15 +400,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
531
400
|
|
|
532
401
|
static readonly #waitDurations: { label: string; ms: number }[] = [];
|
|
533
402
|
|
|
534
|
-
|
|
535
|
-
* Time one wait so a run that dies of *cumulative* slowness can say where the time went.
|
|
536
|
-
*
|
|
537
|
-
* Each wait here is individually bounded, but their budgets sum past the per-test timeout — `startHost`
|
|
538
|
-
* (60s) plus one `waitForLogSince` (60s) plus one `waitForHttpText` (60s) already reaches 180s. So a
|
|
539
|
-
* loaded round can kill a test without any single wait failing, and Bun reports only "this test timed out",
|
|
540
|
-
* naming neither the step nor how close the others came. That produced failures that looked like they
|
|
541
|
-
* rotated between tests at random, when what rotates is which long test happened to be slowest.
|
|
542
|
-
*/
|
|
403
|
+
// Bounded waits can still sum past the test timeout, and Bun then names no step, so each one is recorded.
|
|
543
404
|
static async #timed<T>(label: string, work: () => Promise<T>): Promise<T> {
|
|
544
405
|
const at = performance.now();
|
|
545
406
|
try {
|
|
@@ -549,7 +410,6 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
549
410
|
}
|
|
550
411
|
}
|
|
551
412
|
|
|
552
|
-
/** The slowest waits observed, worst first, for an end-of-run report. */
|
|
553
413
|
static waitStats(limit = 5): { label: string; ms: number }[] {
|
|
554
414
|
return [...DevStabilityHarness.#waitDurations].sort((a, b) => b.ms - a.ms).slice(0, limit);
|
|
555
415
|
}
|
|
@@ -568,8 +428,6 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
568
428
|
const body = await this.tryWaitForHttpText(text, timeoutMs);
|
|
569
429
|
if (body) return body;
|
|
570
430
|
const { port, body: last } = this.#lastHttpProbe;
|
|
571
|
-
// The host's own tail matters as much as the response: a 60s wait that ends in a served error page says
|
|
572
|
-
// what the browser saw, but only the dev server's log says why it got into that state.
|
|
573
431
|
const tail = (this.#host?.logs.join("") ?? "").slice(-2_000);
|
|
574
432
|
throw new Error(
|
|
575
433
|
`Timed out waiting for HTTP text ${String(text)} after ${timeoutMs}ms on port ${port}; ` +
|
|
@@ -587,14 +445,9 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
587
445
|
async #pollHttpText(text: string | RegExp, timeoutMs: number): Promise<string | null> {
|
|
588
446
|
const started = Date.now();
|
|
589
447
|
while (Date.now() - started < timeoutMs) {
|
|
590
|
-
// Re-resolved
|
|
591
|
-
// bind a different port, and quietly polling a port nobody listens on for the rest of the budget is
|
|
592
|
-
// indistinguishable from a page that never updated — it cost 60s and a misleading failure.
|
|
448
|
+
// Re-resolved each poll: a dev-host restart can bind a different port.
|
|
593
449
|
const port = await this.resolvePort();
|
|
594
|
-
// Bounded per request
|
|
595
|
-
// mid-recovery accepts the connection and then never answers, so an unbounded `fetch` parks here for
|
|
596
|
-
// as long as the peer likes: measured 225561ms against this method's own 60000ms budget, which then
|
|
597
|
-
// ate the whole 180s test timeout and reported "this test timed out" with no other information.
|
|
450
|
+
// Bounded per request: a recovering dev server can accept a connection and never answer.
|
|
598
451
|
const budget = Math.max(250, Math.min(5_000, timeoutMs - (Date.now() - started)));
|
|
599
452
|
const body = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(budget) })
|
|
600
453
|
.then((res) => res.text())
|
|
@@ -632,15 +485,8 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
632
485
|
throw new Error("Timed out connecting HMR websocket");
|
|
633
486
|
}
|
|
634
487
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
*
|
|
638
|
-
* A raw socket dies whenever the backend restarts, and a dead socket is indistinguishable from a quiet
|
|
639
|
-
* one: `waitForMessageSince` polls an array nothing can ever append to, then reports a plain timeout a
|
|
640
|
-
* minute later. In a 3-way parallel run that was a third of all failures — the diagnostic read
|
|
641
|
-
* `socket=closed since-mark=[] total=3` — and it says nothing about the product, since the real client
|
|
642
|
-
* reconnects and picks the stream back up (`akanjs/server/hmr/clientScript.ts`).
|
|
643
|
-
*/
|
|
488
|
+
// Reconnects like the real client (`akanjs/server/hmr/clientScript.ts`): a socket a backend restart dropped
|
|
489
|
+
// would otherwise read as silence.
|
|
644
490
|
async connectHmrProbe(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<DevStabilityHmrProbe> {
|
|
645
491
|
const messages: unknown[] = [];
|
|
646
492
|
let socket = await this.connectHmr(timeoutMs);
|
|
@@ -656,9 +502,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
656
502
|
}
|
|
657
503
|
});
|
|
658
504
|
ws.addEventListener("close", () => {
|
|
659
|
-
// Bounded
|
|
660
|
-
// down in `cleanup()`, and an unbounded retry there would keep reconnecting at a dead port for a
|
|
661
|
-
// minute after the test ended.
|
|
505
|
+
// Bounded and short: the last close is the teardown in `cleanup()`, where retrying would poll a dead port.
|
|
662
506
|
if (closedByCaller || reconnects >= DevStabilityHarness.#maxProbeReconnects) return;
|
|
663
507
|
void this.connectHmr(Math.min(timeoutMs, 5_000))
|
|
664
508
|
.then((next) => {
|
|
@@ -670,16 +514,12 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
670
514
|
socket = next;
|
|
671
515
|
listen(next);
|
|
672
516
|
})
|
|
673
|
-
// A
|
|
674
|
-
// not something to throw from an event listener.
|
|
517
|
+
// A server gone for good surfaces through the caller's own waits; never throw from a listener.
|
|
675
518
|
.catch(() => undefined);
|
|
676
519
|
});
|
|
677
520
|
};
|
|
678
521
|
listen(socket);
|
|
679
522
|
return {
|
|
680
|
-
get ws() {
|
|
681
|
-
return socket;
|
|
682
|
-
},
|
|
683
523
|
get reconnects() {
|
|
684
524
|
return reconnects;
|
|
685
525
|
},
|
|
@@ -716,60 +556,11 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
716
556
|
}
|
|
717
557
|
}
|
|
718
558
|
|
|
719
|
-
async waitForHmrMessage(
|
|
720
|
-
ws: WebSocket,
|
|
721
|
-
predicate: (message: unknown) => boolean,
|
|
722
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
723
|
-
): Promise<unknown> {
|
|
724
|
-
return await new Promise((resolve, reject) => {
|
|
725
|
-
const timeout = setTimeout(() => {
|
|
726
|
-
ws.removeEventListener("message", onMessage);
|
|
727
|
-
reject(new Error("Timed out waiting for HMR message"));
|
|
728
|
-
}, timeoutMs);
|
|
729
|
-
const onMessage = (event: MessageEvent) => {
|
|
730
|
-
const raw = typeof event.data === "string" ? event.data : "";
|
|
731
|
-
let message: unknown;
|
|
732
|
-
try {
|
|
733
|
-
message = JSON.parse(raw);
|
|
734
|
-
} catch {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (!predicate(message)) return;
|
|
738
|
-
clearTimeout(timeout);
|
|
739
|
-
ws.removeEventListener("message", onMessage);
|
|
740
|
-
resolve(message);
|
|
741
|
-
};
|
|
742
|
-
ws.addEventListener("message", onMessage);
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* The port this fixture's dev server is reachable on — the one the gateway actually bound once it is
|
|
748
|
-
* up, and a probed prediction before that.
|
|
749
|
-
*
|
|
750
|
-
* It used to recompute `8282 + appIndex + randomOffset` from the `apps/` listing on every call, which
|
|
751
|
-
* is wrong twice over. `appIndex` moves whenever any *other* fixture appears or disappears, and two
|
|
752
|
-
* suites running in parallel create and delete one every few seconds — so the answer changed
|
|
753
|
-
* mid-test and every HTTP wait then polled a port nobody was listening on. And the random offset
|
|
754
|
-
* collides, roughly 1-in-N per pair, while the gateway has no fallback for its http port
|
|
755
|
-
* (`AkanApp.start` logs "already in use" and exits), so a collision reads as a boot timeout with no
|
|
756
|
-
* hint of a port problem. Both failures are indistinguishable from a product regression, which is
|
|
757
|
-
* most of why a red run of this suite could not be told apart from noise.
|
|
758
|
-
*/
|
|
759
559
|
async resolvePort(): Promise<number> {
|
|
760
560
|
return this.#gatewayPortFromLogs() ?? this.#boundPort ?? (await this.#allocatePort()).port;
|
|
761
561
|
}
|
|
762
562
|
|
|
763
|
-
|
|
764
|
-
* The port the *most recent* gateway bound, or null before one has said.
|
|
765
|
-
*
|
|
766
|
-
* Read fresh rather than adopted once, because the port can move mid-test: a config edit restarts the dev
|
|
767
|
-
* host, and the replacement recomputes its own port from the `apps/` listing (`AppExecutor.getDevPort`) —
|
|
768
|
-
* which a parallel run has changed in the meantime. Holding the boot-time port meant the config test
|
|
769
|
-
* watched the restart succeed and then timed out fetching from the address the *old* gateway had used.
|
|
770
|
-
* (Worth knowing outside the tests too: adding or removing an app during a session moves a running dev
|
|
771
|
-
* server's port at its next restart.)
|
|
772
|
-
*/
|
|
563
|
+
// The latest match: a restarted dev host recomputes its port from the `apps/` listing, which may have moved.
|
|
773
564
|
#gatewayPortFromLogs(): number | null {
|
|
774
565
|
const logs = this.#host?.logs;
|
|
775
566
|
if (!logs) return null;
|
|
@@ -779,14 +570,11 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
779
570
|
return latest;
|
|
780
571
|
}
|
|
781
572
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
* first host is still shutting down and holding its port; the probe covers everything outside this
|
|
785
|
-
* process; and the pid seed keeps two concurrent runs out of each other's band. */
|
|
573
|
+
// A pid-seeded, forward-only cursor probed against the OS: no two harnesses in a process share an offset while
|
|
574
|
+
// one still shuts down, and concurrent runs start in different places.
|
|
786
575
|
static readonly portOffsetMin = 3_000;
|
|
787
576
|
static readonly portOffsetMax = 4_000;
|
|
788
|
-
|
|
789
|
-
* fixture shifts the index. A stride wider than any plausible drift keeps them distinct. */
|
|
577
|
+
// The port is `8282 + appIndex + offset`: a stride wider than any index drift keeps neighbouring offsets apart.
|
|
790
578
|
static readonly portOffsetStride = 4;
|
|
791
579
|
static #portOffsetCursor: number | null = null;
|
|
792
580
|
|
|
@@ -807,8 +595,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
807
595
|
);
|
|
808
596
|
}
|
|
809
597
|
|
|
810
|
-
|
|
811
|
-
* the port on any address the app might pick. */
|
|
598
|
+
// The default interface is the strict test: a bind there fails if any address the app might pick is held.
|
|
812
599
|
static async isPortFree(port: number): Promise<boolean> {
|
|
813
600
|
try {
|
|
814
601
|
Bun.serve({ port, fetch: () => new Response("probe") }).stop(true);
|
|
@@ -831,8 +618,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
831
618
|
for (let attempt = 0; attempt < DevStabilityHarness.#portOffsetSlots; attempt++) {
|
|
832
619
|
const offset = DevStabilityHarness.#nextPortOffset();
|
|
833
620
|
const port = basePort + offset;
|
|
834
|
-
//
|
|
835
|
-
// both have to be free for a boot to be clean.
|
|
621
|
+
// Child 0's websocket port is `port + 10_000` (`AkanApp` `#wsBasePort`), so both must be free.
|
|
836
622
|
if ((await DevStabilityHarness.isPortFree(port)) && (await DevStabilityHarness.isPortFree(port + 10_000)))
|
|
837
623
|
return { port, offset };
|
|
838
624
|
}
|
|
@@ -841,8 +627,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
841
627
|
);
|
|
842
628
|
}
|
|
843
629
|
|
|
844
|
-
|
|
845
|
-
* locale-sorted): counting stray entries like .DS_Store would put us one port off the gateway. */
|
|
630
|
+
// Mirrors `getDevPort()`: dirs holding an akan.config.ts, locale-sorted; a stray entry would shift the port.
|
|
846
631
|
async #appIndex(): Promise<number> {
|
|
847
632
|
const appsDir = path.join(this.workspaceRoot, "apps");
|
|
848
633
|
const entries = await readdir(appsDir, { withFileTypes: true }).catch(() => []);
|
|
@@ -859,7 +644,6 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
859
644
|
return Math.max(apps.indexOf(this.appName), 0);
|
|
860
645
|
}
|
|
861
646
|
|
|
862
|
-
/** Stop predicting once the gateway has said what it bound. */
|
|
863
647
|
async #adoptBoundPort(host: DevStabilityHost): Promise<void> {
|
|
864
648
|
const match = await host
|
|
865
649
|
.waitForLog(/AkanApp gateway is running on port http:\/\/localhost:(\d+)/, 30_000)
|
|
@@ -872,13 +656,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
872
656
|
console.warn(`[harness] ${this.appName} bound port ${bound}, not the predicted ${predicted}`);
|
|
873
657
|
}
|
|
874
658
|
|
|
875
|
-
|
|
876
|
-
* Fixtures and dev hosts left behind by a test process that died before `cleanup()` could run.
|
|
877
|
-
*
|
|
878
|
-
* They keep watching a deleted app, hold their ports, and — because the fixture directory is still in
|
|
879
|
-
* `apps/` — shift the app index every other harness predicts its port from. Only fixtures whose
|
|
880
|
-
* owning test pid is gone are swept, so a concurrent run's live fixtures are never touched.
|
|
881
|
-
*/
|
|
659
|
+
// Only fixtures whose owning test pid is gone, so a concurrent run's live fixtures are never touched.
|
|
882
660
|
static async sweepAbandonedFixtures(workspaceRoot: string): Promise<string[]> {
|
|
883
661
|
const appsDir = path.join(workspaceRoot, "apps");
|
|
884
662
|
const entries = await readdir(appsDir, { withFileTypes: true }).catch(() => []);
|
|
@@ -908,15 +686,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
908
686
|
}
|
|
909
687
|
}
|
|
910
688
|
|
|
911
|
-
|
|
912
|
-
* Resident set size of the whole `akan start` process tree, which is what a dev sandbox actually
|
|
913
|
-
* costs: supervisor, builder, gateway, replica and rsc worker. `excludeBuilder` drops the bundler
|
|
914
|
-
* process, whose growth across saves is `Bun.build` native arena retention rather than a leak the
|
|
915
|
-
* other processes could be blamed for.
|
|
916
|
-
*/
|
|
917
|
-
// Matched precisely, not by directory: the disposable build worker lives at
|
|
918
|
-
// `incrementalBuilder/buildBatch.proc.ts`, so a substring match on `incrementalBuilder` would count
|
|
919
|
-
// the worker as the watcher and make every builder measurement depend on spawn timing.
|
|
689
|
+
// Not `incrementalBuilder`: the disposable worker `incrementalBuilder/buildBatch.proc.ts` would match too.
|
|
920
690
|
static readonly #builderCmd = "incrementalBuilder.proc";
|
|
921
691
|
static readonly #buildWorkerCmd = "buildBatch.proc";
|
|
922
692
|
|
|
@@ -936,15 +706,10 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
936
706
|
);
|
|
937
707
|
}
|
|
938
708
|
|
|
939
|
-
/**
|
|
940
|
-
* The long-lived builder process on its own: its RSS is what bundler-arena retention used to move,
|
|
941
|
-
* and its pid is what changes when the host recycles it, so a bounded-builder assertion needs both.
|
|
942
|
-
*/
|
|
943
709
|
static async builderProcess(rootPid: number): Promise<{ pid: number; rssBytes: number } | null> {
|
|
944
710
|
return DevStabilityHarness.#findProcess(rootPid, DevStabilityHarness.#builderCmd);
|
|
945
711
|
}
|
|
946
712
|
|
|
947
|
-
/** The disposable per-generation build worker, which should only exist while a build is running. */
|
|
948
713
|
static async buildWorkerProcess(rootPid: number): Promise<{ pid: number; rssBytes: number } | null> {
|
|
949
714
|
return DevStabilityHarness.#findProcess(rootPid, DevStabilityHarness.#buildWorkerCmd);
|
|
950
715
|
}
|
|
@@ -956,26 +721,15 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
956
721
|
return found ? { pid: found.pid, rssBytes: found.rssKb * 1024 } : null;
|
|
957
722
|
}
|
|
958
723
|
|
|
959
|
-
|
|
724
|
+
// Deepest first, so callers signal children before parents.
|
|
960
725
|
static async descendantPids(rootPid: number | undefined): Promise<number[]> {
|
|
961
726
|
if (!rootPid) return [];
|
|
962
727
|
const pids = DevStabilityHarness.#collectDescendants(await DevStabilityHarness.#psRowsOrEmpty(), rootPid);
|
|
963
728
|
return [...pids].reverse();
|
|
964
729
|
}
|
|
965
730
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
*
|
|
969
|
-
* Bounded because the unbounded version hung indefinitely under parallel load, and it was the single
|
|
970
|
-
* root cause of every remaining cleanup failure: a `ps` that never returns became an `afterEach` timeout,
|
|
971
|
-
* and once a `beforeAll` timeout that cost an entire shard — 0 of 16 tests ran. The watchdog named it
|
|
972
|
-
* outright (`descendants still running after 10000ms`). It also spawns `ps` directly rather than through
|
|
973
|
-
* `Bun.$`, whose hung shells were what the runner then reported as `killed 1 dangling process`.
|
|
974
|
-
*
|
|
975
|
-
* `null` rather than `[]` on purpose: an empty list is a legitimate answer to "is the builder running",
|
|
976
|
-
* so collapsing the two would let a measurement read a timeout as "no such process" and quietly assert
|
|
977
|
-
* the opposite of what it meant to.
|
|
978
|
-
*/
|
|
731
|
+
// `ps` hangs under parallel load, so it is bounded and spawned directly (a hung `Bun.$` shell leaks). `null`, not
|
|
732
|
+
// `[]`, means "could not look", which a measurement must never read as "nothing running".
|
|
979
733
|
static readonly #psTimeoutMs = 5_000;
|
|
980
734
|
|
|
981
735
|
static async #psRows(): Promise<Array<{ pid: number; ppid: number; rssKb: number; cmd: string }> | null> {
|
|
@@ -987,8 +741,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
987
741
|
stdin: "ignore",
|
|
988
742
|
});
|
|
989
743
|
} catch (error) {
|
|
990
|
-
//
|
|
991
|
-
// a timeout, and for the same reason: this is "could not look", not "nothing is running".
|
|
744
|
+
// Slim images such as `oven/bun` ship without procps.
|
|
992
745
|
console.warn(`[harness] could not run ps: ${error instanceof Error ? error.message : String(error)}`);
|
|
993
746
|
return null;
|
|
994
747
|
}
|
|
@@ -1012,13 +765,10 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
1012
765
|
});
|
|
1013
766
|
}
|
|
1014
767
|
|
|
1015
|
-
/** For callers that kill: an incomplete list is survivable, because they also signal the tracked child
|
|
1016
|
-
* directly, and hanging instead would cost the whole test. */
|
|
1017
768
|
static async #psRowsOrEmpty(): Promise<Array<{ pid: number; ppid: number; rssKb: number; cmd: string }>> {
|
|
1018
769
|
return (await DevStabilityHarness.#psRows()) ?? [];
|
|
1019
770
|
}
|
|
1020
771
|
|
|
1021
|
-
/** For callers that measure: a missing process list must fail loudly, never read as "nothing running". */
|
|
1022
772
|
static async #psRowsOrThrow(): Promise<Array<{ pid: number; ppid: number; rssKb: number; cmd: string }>> {
|
|
1023
773
|
const rows = (await DevStabilityHarness.#psRows()) ?? (await DevStabilityHarness.#psRows());
|
|
1024
774
|
if (!rows) throw new Error("ps did not answer twice in a row; cannot measure the process tree");
|
|
@@ -1026,10 +776,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
|
|
|
1026
776
|
}
|
|
1027
777
|
|
|
1028
778
|
static #collectDescendants(rows: Array<{ pid: number; ppid: number }>, rootPid: number): Set<number> {
|
|
1029
|
-
//
|
|
1030
|
-
// shell -> dev host -> gateway -> replica -> rsc worker, and a pass only guarantees one new
|
|
1031
|
-
// generation when `ps` happens to list parents before children. A capped walk silently leaves the
|
|
1032
|
-
// deepest processes unsignalled, which is how orphaned dev hosts survived `stop()`.
|
|
779
|
+
// To a fixpoint, not a fixed depth: the tree is seven levels deep and `ps` lists it in no particular order.
|
|
1033
780
|
const pids = new Set([rootPid]);
|
|
1034
781
|
for (let added = 1; added > 0; ) {
|
|
1035
782
|
added = 0;
|
|
@@ -1066,8 +813,6 @@ export async function waitForHmrMessageSince(
|
|
|
1066
813
|
if (found) return found;
|
|
1067
814
|
await wait(50);
|
|
1068
815
|
}
|
|
1069
|
-
// "Timed out" alone cannot distinguish the two cases that matter: the message was never published, or
|
|
1070
|
-
// the socket died and nothing could have arrived. Both look identical from the message array.
|
|
1071
816
|
throw new Error(
|
|
1072
817
|
`Timed out waiting for HMR message since mark ${mark}${describeProbe ? ` (${describeProbe()})` : ""}`,
|
|
1073
818
|
);
|
|
@@ -1087,16 +832,6 @@ export async function waitForNoHmrMessageSince(
|
|
|
1087
832
|
}
|
|
1088
833
|
}
|
|
1089
834
|
|
|
1090
|
-
export async function waitForLog(
|
|
1091
|
-
logs: string[],
|
|
1092
|
-
pattern: RegExp,
|
|
1093
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
1094
|
-
): Promise<RegExpMatchArray> {
|
|
1095
|
-
return await waitForLogSince(logs, 0, pattern, timeoutMs);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
export const markLog = (logs: string[]): number => logs.join("").length;
|
|
1099
|
-
|
|
1100
835
|
export async function waitForLogSince(
|
|
1101
836
|
logs: string[],
|
|
1102
837
|
mark: number,
|