@opengsd/gsd-pi 1.10.0 → 1.11.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.
Files changed (218) hide show
  1. package/README.md +10 -9
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
  4. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-verification.js +25 -12
  6. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
  7. package/dist/resources/extensions/gsd/db/engine.js +8 -2
  8. package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
  9. package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
  12. package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
  13. package/dist/resources/extensions/gsd/model-router.js +10 -5
  14. package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
  15. package/dist/resources/extensions/gsd/preferences-models.js +50 -10
  16. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  17. package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
  18. package/dist/resources/extensions/gsd/preferences.js +7 -0
  19. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  20. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
  21. package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
  22. package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
  23. package/dist/resources/extensions/subagent/index.js +1 -1
  24. package/dist/startup-model-validation.d.ts +1 -1
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  53. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  58. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  59. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  60. package/integrations/hermes/pyproject.toml +1 -1
  61. package/package.json +7 -6
  62. package/packages/cloud-mcp-gateway/package.json +2 -2
  63. package/packages/contracts/dist/rpc.d.ts +1 -1
  64. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  65. package/packages/contracts/dist/rpc.js +1 -1
  66. package/packages/contracts/dist/rpc.js.map +1 -1
  67. package/packages/contracts/dist/rpc.test.js +1 -1
  68. package/packages/contracts/dist/rpc.test.js.map +1 -1
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/daemon/package.json +4 -4
  71. package/packages/gsd-agent-core/package.json +5 -5
  72. package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
  73. package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  83. package/packages/gsd-agent-modes/package.json +7 -7
  84. package/packages/gsd-cloud/README.md +12 -2
  85. package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
  86. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
  87. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cli.js +36 -12
  89. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  90. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
  91. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  92. package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
  93. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  94. package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
  95. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
  96. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
  97. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
  98. package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
  99. package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
  100. package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
  101. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
  102. package/packages/gsd-cloud/dist/runtime-process.js +429 -61
  103. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
  104. package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
  105. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
  106. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
  107. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
  108. package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
  109. package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
  110. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
  111. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
  112. package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
  113. package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
  114. package/packages/gsd-cloud/package.json +2 -2
  115. package/packages/mcp-server/package.json +4 -4
  116. package/packages/native/package.json +1 -1
  117. package/packages/pi-agent-core/dist/types.d.ts +2 -2
  118. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  119. package/packages/pi-agent-core/dist/types.js.map +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/models.generated.d.ts +113 -2
  123. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  124. package/packages/pi-ai/dist/models.generated.js +84 -9
  125. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  126. package/packages/pi-ai/dist/models.js +18 -6
  127. package/packages/pi-ai/dist/models.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  129. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  130. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  131. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  134. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  135. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  136. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  138. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  140. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  141. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  142. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  143. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  145. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +1 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/types.d.ts +9 -1
  151. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/types.js.map +1 -1
  153. package/packages/pi-ai/package.json +1 -1
  154. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  156. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
  160. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
  163. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  164. package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
  165. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  166. package/packages/pi-coding-agent/package.json +7 -7
  167. package/packages/pi-tui/package.json +2 -2
  168. package/packages/rpc-client/package.json +2 -2
  169. package/pkg/dist/theme/theme.d.ts +1 -1
  170. package/pkg/dist/theme/theme.d.ts.map +1 -1
  171. package/pkg/dist/theme/theme.js +1 -0
  172. package/pkg/dist/theme/theme.js.map +1 -1
  173. package/pkg/package.json +1 -1
  174. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
  175. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  176. package/src/resources/extensions/gsd/auto-verification.ts +46 -12
  177. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
  178. package/src/resources/extensions/gsd/db/engine.ts +9 -1
  179. package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
  180. package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
  181. package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  182. package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
  183. package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
  184. package/src/resources/extensions/gsd/model-router.ts +10 -5
  185. package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
  186. package/src/resources/extensions/gsd/preferences-models.ts +54 -10
  187. package/src/resources/extensions/gsd/preferences-types.ts +10 -2
  188. package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
  189. package/src/resources/extensions/gsd/preferences.ts +13 -0
  190. package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  191. package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
  192. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
  193. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
  194. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
  195. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  197. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
  198. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
  201. package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
  202. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
  204. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
  205. package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
  206. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
  207. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
  208. package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
  209. package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
  210. package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
  211. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
  213. package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
  214. package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
  215. package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
  216. package/src/resources/extensions/subagent/index.ts +1 -1
  217. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
@@ -1,90 +1,528 @@
1
1
  // Project/App: Open GSD
2
2
  // File Purpose: Regression coverage for detached cloud runtime process timing and shutdown.
3
3
  import { spawn } from "node:child_process";
4
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { test } from "node:test";
8
8
  import assert from "node:assert/strict";
9
9
  import { CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS } from "./cloud-runtime.js";
10
- import { BACKGROUND_RUNTIME_READY_TIMEOUT_MS, backgroundRuntimeStatus, startBackgroundRuntime, stopBackgroundRuntime, } from "./runtime-process.js";
10
+ import { BACKGROUND_RUNTIME_READY_TIMEOUT_MS, acquireRuntimeStartLock, backgroundRuntimeStatus, commandLineMatchesRuntimeConfig, runtimeLogPath, runtimeStatePath, startBackgroundRuntime, stopBackgroundRuntime, writeRuntimeState, } from "./runtime-process.js";
11
+ import { runtimeArtifactPath } from "./runtime-artifacts.js";
12
+ import { runtimeTelemetryPath } from "./runtime-telemetry.js";
11
13
  test("background startup allows the cloud runtime's full initial reconnect window", () => {
12
14
  assert.ok(BACKGROUND_RUNTIME_READY_TIMEOUT_MS > CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS);
13
15
  });
14
- test("stop waits for the detached runtime to exit before removing its state", { timeout: 5_000 }, async () => {
15
- const root = mkdtempSync(join(tmpdir(), "gsd-cloud-stop-"));
16
+ test("runtime artifacts are namespaced by config path while daemon.yaml stays legacy-compatible", (t) => {
17
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-artifacts-"));
18
+ t.after(() => rmSync(root, { recursive: true, force: true }));
19
+ const defaultConfig = join(root, "daemon.yaml");
20
+ const firstConfig = join(root, "first.yaml");
21
+ const secondConfig = join(root, "second.yaml");
22
+ assert.equal(runtimeStatePath(defaultConfig), join(root, "cloud-runtime.json"));
23
+ assert.equal(runtimeLogPath(defaultConfig), join(root, "cloud-runtime.log"));
24
+ assert.equal(runtimeTelemetryPath(defaultConfig), join(root, "cloud-runtime-status.json"));
25
+ assert.notEqual(runtimeStatePath(firstConfig), runtimeStatePath(secondConfig));
26
+ assert.notEqual(runtimeLogPath(firstConfig), runtimeLogPath(secondConfig));
27
+ assert.notEqual(runtimeTelemetryPath(firstConfig), runtimeTelemetryPath(secondConfig));
28
+ });
29
+ test("runtime artifact namespace is stable across monitor implementations", () => {
30
+ assert.equal(runtimeStatePath("/work/state/first.yaml"), "/work/state/cloud-runtime-58cb3ff924131c6e.json");
31
+ });
32
+ test("stop refuses to signal a process whose identity does not match state", async (t) => {
33
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-reused-pid-"));
16
34
  const configPath = join(root, "daemon.yaml");
17
35
  const statePath = join(root, "cloud-runtime.json");
36
+ const child = spawn(process.execPath, ["-e", "setInterval(()=>{},1000)"]);
37
+ t.after(() => {
38
+ child.kill("SIGKILL");
39
+ rmSync(root, { recursive: true, force: true });
40
+ });
41
+ assert.ok(child.pid);
42
+ writeFileSync(statePath, `${JSON.stringify({
43
+ pid: child.pid,
44
+ projects: [root],
45
+ process_start_identity: "not-this-process",
46
+ })}\n`);
47
+ assert.equal(await stopBackgroundRuntime(configPath), false);
48
+ assert.equal(processIsRunning(child.pid), true);
49
+ assert.equal(existsSync(statePath), true);
50
+ });
51
+ test("custom configs migrate matching live legacy runtime state", (t) => {
52
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-legacy-state-"));
53
+ const configPath = join(root, "custom.yaml");
54
+ const legacyStatePath = join(root, "cloud-runtime.json");
18
55
  const child = spawn(process.execPath, [
19
- "-e",
20
- "process.on('SIGTERM',()=>setTimeout(()=>process.exit(0),200));process.send?.('ready');setInterval(()=>{},1000)",
21
- ], { stdio: ["ignore", "ignore", "ignore", "ipc"] });
22
- try {
23
- await new Promise((resolve, reject) => {
24
- child.once("error", reject);
25
- child.once("message", () => resolve());
56
+ writeLegacyRuntime(root, "gsd-cloud.js"),
57
+ "_run",
58
+ "--config",
59
+ configPath,
60
+ ]);
61
+ t.after(() => {
62
+ child.kill("SIGKILL");
63
+ rmSync(root, { recursive: true, force: true });
64
+ });
65
+ assert.ok(child.pid);
66
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
67
+ const status = backgroundRuntimeStatus(configPath);
68
+ assert.equal(status.running, true);
69
+ assert.equal(status.pid, child.pid);
70
+ assert.equal(existsSync(runtimeStatePath(configPath)), true);
71
+ assert.equal(existsSync(legacyStatePath), false);
72
+ });
73
+ for (const runtimeArgs of [
74
+ ["connect"],
75
+ ["login", "--foreground"],
76
+ ]) {
77
+ test(`custom configs migrate legacy ${runtimeArgs.join(" ")} runtime state`, async (t) => {
78
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-foreground-legacy-"));
79
+ const configPath = join(root, "custom runtime.yaml");
80
+ const legacyStatePath = join(root, "cloud-runtime.json");
81
+ const binaryPath = writeLegacyRuntime(root, "gsd-cloud.js");
82
+ const child = spawn(process.execPath, [
83
+ binaryPath,
84
+ ...runtimeArgs,
85
+ "--config",
86
+ configPath,
87
+ ]);
88
+ t.after(() => {
89
+ child.kill("SIGKILL");
90
+ rmSync(root, { recursive: true, force: true });
26
91
  });
27
92
  assert.ok(child.pid);
28
- writeFileSync(statePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
29
- const startedAt = Date.now();
30
- assert.equal(await stopBackgroundRuntime(configPath), true);
31
- assert.ok(Date.now() - startedAt >= 150);
32
- assert.equal(existsSync(statePath), false);
33
- }
34
- finally {
93
+ await waitForCondition(() => processIsRunning(child.pid));
94
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
95
+ const status = backgroundRuntimeStatus(configPath);
96
+ assert.equal(status.running, true);
97
+ assert.equal(status.pid, child.pid);
98
+ assert.equal(existsSync(runtimeStatePath(configPath)), true);
99
+ assert.equal(existsSync(legacyStatePath), false);
100
+ });
101
+ }
102
+ test("legacy runtime state for another config is not migrated or signalled", async (t) => {
103
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-foreign-legacy-state-"));
104
+ const requestedConfig = join(root, "requested.yaml");
105
+ const actualConfig = join(root, "actual.yaml");
106
+ const legacyStatePath = join(root, "cloud-runtime.json");
107
+ const child = spawn(process.execPath, [
108
+ "-e",
109
+ "setInterval(()=>{},1000)",
110
+ "_run",
111
+ "--config",
112
+ actualConfig,
113
+ ]);
114
+ t.after(() => {
35
115
  child.kill("SIGKILL");
36
116
  rmSync(root, { recursive: true, force: true });
37
- }
117
+ });
118
+ assert.ok(child.pid);
119
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
120
+ assert.equal(await stopBackgroundRuntime(requestedConfig), false);
121
+ assert.equal(processIsRunning(child.pid), true);
122
+ assert.equal(existsSync(legacyStatePath), true);
38
123
  });
39
- test("concurrent starts serialize and leave only the newest runtime running", { timeout: 15_000 }, async () => {
40
- const root = mkdtempSync(join(tmpdir(), "gsd-cloud-concurrent-start-"));
124
+ test("default config preserves shared legacy state owned by a custom config", async (t) => {
125
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-shared-legacy-state-"));
126
+ const requestedConfig = join(root, "daemon.yaml");
127
+ const actualConfig = join(root, "custom.yaml");
128
+ const legacyStatePath = join(root, "cloud-runtime.json");
129
+ const binaryPath = writeLegacyRuntime(root, "gsd-cloud.js");
130
+ const child = spawn(process.execPath, [binaryPath, "connect", "--config", actualConfig]);
131
+ t.after(() => {
132
+ child.kill("SIGKILL");
133
+ rmSync(root, { recursive: true, force: true });
134
+ });
135
+ assert.ok(child.pid);
136
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
137
+ assert.equal(await stopBackgroundRuntime(requestedConfig), false);
138
+ assert.equal(processIsRunning(child.pid), true);
139
+ assert.equal(existsSync(legacyStatePath), true);
140
+ });
141
+ test("legacy migration rejects unrelated executables", async (t) => {
142
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-unrelated-legacy-state-"));
143
+ const configPath = join(root, "daemon.yaml");
144
+ const legacyStatePath = join(root, "cloud-runtime.json");
145
+ const binaryPath = writeLegacyRuntime(root, "other-tool.js");
146
+ const child = spawn(process.execPath, [binaryPath, "connect", "--config", configPath]);
147
+ t.after(() => {
148
+ child.kill("SIGKILL");
149
+ rmSync(root, { recursive: true, force: true });
150
+ });
151
+ assert.ok(child.pid);
152
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
153
+ assert.equal(await stopBackgroundRuntime(configPath), false);
154
+ assert.equal(processIsRunning(child.pid), true);
155
+ assert.equal(existsSync(legacyStatePath), true);
156
+ });
157
+ test("legacy migration preserves config paths with spaces before short options", async (t) => {
158
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-argv-legacy-"));
159
+ const configPath = join(root, "custom runtime.yaml");
160
+ const legacyStatePath = join(root, "cloud-runtime.json");
161
+ const binaryPath = writeLegacyRuntime(root, "gsd-cloud.js");
162
+ const child = spawn(process.execPath, [
163
+ binaryPath,
164
+ "connect",
165
+ "--config",
166
+ configPath,
167
+ "-v",
168
+ "--foreground",
169
+ ]);
170
+ t.after(() => {
171
+ child.kill("SIGKILL");
172
+ rmSync(root, { recursive: true, force: true });
173
+ });
174
+ assert.ok(child.pid);
175
+ await waitForCondition(() => processIsRunning(child.pid));
176
+ writeFileSync(legacyStatePath, `${JSON.stringify({ pid: child.pid, projects: [root] })}\n`);
177
+ const status = backgroundRuntimeStatus(configPath);
178
+ assert.equal(status.running, true);
179
+ assert.equal(status.pid, child.pid);
180
+ assert.equal(existsSync(runtimeStatePath(configPath)), true);
181
+ assert.equal(existsSync(legacyStatePath), false);
182
+ });
183
+ test("Windows command matching accepts quoted gsd-cloud.js paths", () => {
184
+ const configPath = "/work/runtime/daemon.yaml";
185
+ const command = [
186
+ '"C:\\Program Files\\nodejs\\node.exe"',
187
+ '"C:\\Program Files\\gsd-cloud\\gsd-cloud.js"',
188
+ "connect",
189
+ "--config",
190
+ `"${configPath}"`,
191
+ ].join(" ");
192
+ assert.equal(commandLineMatchesRuntimeConfig(command, configPath, "win32"), true);
193
+ });
194
+ test("command matching rejects gsd-cloud decoys before another launcher", () => {
195
+ const configPath = "/work/runtime/daemon.yaml";
196
+ assert.equal(commandLineMatchesRuntimeConfig(`other-tool /tmp/gsd-cloud.js connect --config ${configPath}`, configPath, "linux"), false);
197
+ assert.equal(commandLineMatchesRuntimeConfig(`"C:\\Tools\\other-tool.exe" "C:\\tmp\\gsd-cloud.js" connect --config "${configPath}"`, configPath, "win32"), false);
198
+ assert.equal(commandLineMatchesRuntimeConfig(`/usr/bin/node /tmp/gsd-cloud.js --config ${configPath} connect --config /other.yaml`, configPath, "linux"), false);
199
+ assert.equal(commandLineMatchesRuntimeConfig(`/usr/bin/other /tmp/gsd-cloud.js connect --config ${configPath}`, configPath, "darwin"), false);
200
+ assert.equal(commandLineMatchesRuntimeConfig(`/usr/bin/node /usr/bin/other /tmp/gsd-cloud.js connect --config ${configPath}`, configPath, "darwin"), false);
201
+ });
202
+ test("command matching accepts a directly launched bare gsd-cloud executable", () => {
203
+ const configPath = "/work/runtime/daemon.yaml";
204
+ assert.equal(commandLineMatchesRuntimeConfig(`gsd-cloud connect --config ${configPath}`, configPath, "darwin"), true);
205
+ assert.equal(commandLineMatchesRuntimeConfig(`"/Applications/GSD Cloud/gsd-cloud" connect --config ${configPath}`, configPath, "darwin"), true);
206
+ });
207
+ test("start lock atomically records PID and process identity", { timeout: 5_000 }, async (t) => {
208
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-lock-owner-"));
209
+ const configPath = join(root, "daemon.yaml");
210
+ const binaryPath = writeReadyRuntime(root, 500);
211
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
212
+ t.after(async () => {
213
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
214
+ rmSync(root, { recursive: true, force: true });
215
+ });
216
+ const starting = startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] });
217
+ await waitForCondition(() => existsSync(lockPath));
218
+ const owner = JSON.parse(readFileSync(lockPath, "utf8"));
219
+ assert.equal(owner.pid, process.pid);
220
+ assert.equal(typeof owner.process_start_identity, "string");
221
+ assert.ok(owner.process_start_identity);
222
+ await starting;
223
+ });
224
+ test("start lock recovers immediately when its PID identity is stale", { timeout: 5_000 }, async (t) => {
225
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-stale-lock-"));
41
226
  const configPath = join(root, "daemon.yaml");
42
227
  const binaryPath = writeReadyRuntime(root);
43
- try {
44
- const [first, second] = await Promise.all([
45
- startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] }),
46
- startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] }),
228
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
229
+ writeFileSync(lockPath, `${JSON.stringify({
230
+ pid: process.pid,
231
+ process_start_identity: "reused-process",
232
+ })}\n`);
233
+ t.after(async () => {
234
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
235
+ rmSync(root, { recursive: true, force: true });
236
+ });
237
+ const starting = startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] });
238
+ await new Promise((resolveImmediate) => setImmediate(resolveImmediate));
239
+ const owner = JSON.parse(readFileSync(lockPath, "utf8"));
240
+ assert.notEqual(owner.process_start_identity, "reused-process");
241
+ await starting;
242
+ });
243
+ test("start lock recovery never removes a replacement owner", { timeout: 5_000 }, async (t) => {
244
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-replaced-lock-"));
245
+ const ownerConfigPath = join(root, "owner.yaml");
246
+ const configPath = join(root, "daemon.yaml");
247
+ const ownerLockPath = runtimeArtifactPath(ownerConfigPath, "start.lock");
248
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
249
+ const releaseOwner = await acquireRuntimeStartLock(ownerConfigPath);
250
+ const replacementOwner = readFileSync(ownerLockPath, "utf8");
251
+ releaseOwner();
252
+ writeFileSync(lockPath, "stale");
253
+ const old = new Date(0);
254
+ utimesSync(lockPath, old, old);
255
+ let release;
256
+ t.after(async () => {
257
+ rmSync(lockPath, { force: true });
258
+ release ??= await acquiring.catch(() => undefined);
259
+ release?.();
260
+ rmSync(root, { recursive: true, force: true });
261
+ });
262
+ let replacementInstalled;
263
+ let replacementInode;
264
+ const installed = new Promise((resolve) => {
265
+ replacementInstalled = resolve;
266
+ });
267
+ const acquiring = acquireRuntimeStartLock(configPath, () => {
268
+ rmSync(lockPath);
269
+ writeFileSync(lockPath, replacementOwner);
270
+ replacementInode = statSync(lockPath, { bigint: true }).ino;
271
+ replacementInstalled();
272
+ });
273
+ await installed;
274
+ assert.equal(readFileSync(lockPath, "utf8"), replacementOwner);
275
+ assert.equal(statSync(lockPath, { bigint: true }).ino, replacementInode);
276
+ rmSync(lockPath);
277
+ release = await acquiring;
278
+ release();
279
+ });
280
+ test("start lock recovery ignores an abandoned recovery claim", { timeout: 5_000 }, async (t) => {
281
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-abandoned-recovery-"));
282
+ const configPath = join(root, "daemon.yaml");
283
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
284
+ const recoveryPath = `${lockPath}.recovery`;
285
+ writeFileSync(lockPath, "stale");
286
+ writeFileSync(recoveryPath, "abandoned");
287
+ const old = new Date(0);
288
+ utimesSync(lockPath, old, old);
289
+ utimesSync(recoveryPath, old, old);
290
+ let release;
291
+ const acquiring = acquireRuntimeStartLock(configPath);
292
+ t.after(async () => {
293
+ rmSync(lockPath, { force: true });
294
+ rmSync(recoveryPath, { force: true });
295
+ release ??= await acquiring.catch(() => undefined);
296
+ release?.();
297
+ rmSync(root, { recursive: true, force: true });
298
+ });
299
+ release = await Promise.race([
300
+ acquiring,
301
+ new Promise((_resolve, reject) => {
302
+ setTimeout(() => reject(new Error("abandoned recovery claim blocked start")), 500);
303
+ }),
304
+ ]);
305
+ release();
306
+ assert.equal(existsSync(lockPath), false);
307
+ assert.equal(readFileSync(recoveryPath, "utf8"), "abandoned");
308
+ });
309
+ test("start lock does not reclaim a live legacy owner by age", { timeout: 5_000 }, async (t) => {
310
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-live-legacy-lock-"));
311
+ const configPath = join(root, "daemon.yaml");
312
+ const binaryPath = writeReadyRuntime(root);
313
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
314
+ writeFileSync(lockPath, `${process.pid}\n`);
315
+ t.after(async () => {
316
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
317
+ rmSync(root, { recursive: true, force: true });
318
+ });
319
+ const starting = startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] });
320
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 1_200));
321
+ assert.equal(readFileSync(lockPath, "utf8"), `${process.pid}\n`);
322
+ rmSync(lockPath);
323
+ await starting;
324
+ });
325
+ test("start lock preserves an aged legacy owner running the same config", { timeout: 5_000 }, async (t) => {
326
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-live-runtime-lock-"));
327
+ const configPath = join(root, "daemon.yaml");
328
+ const binaryPath = writeReadyRuntime(root);
329
+ const legacyBinaryPath = writeLegacyRuntime(root, "gsd-cloud");
330
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
331
+ const owner = spawn(process.execPath, [legacyBinaryPath, "connect", "--config", configPath]);
332
+ t.after(async () => {
333
+ owner.kill("SIGKILL");
334
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
335
+ rmSync(root, { recursive: true, force: true });
336
+ });
337
+ assert.ok(owner.pid);
338
+ await waitForCondition(() => processIsRunning(owner.pid));
339
+ writeFileSync(lockPath, `${owner.pid}\n`);
340
+ const old = new Date(0);
341
+ utimesSync(lockPath, old, old);
342
+ const starting = startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] });
343
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 200));
344
+ assert.equal(readFileSync(lockPath, "utf8"), `${owner.pid}\n`);
345
+ rmSync(lockPath);
346
+ await starting;
347
+ });
348
+ for (const staleOwner of ["", "not-json", `${process.pid}\n`]) {
349
+ test(`start lock recovers stale legacy owner ${JSON.stringify(staleOwner)}`, { timeout: 2_000 }, async (t) => {
350
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-stale-legacy-lock-"));
351
+ const configPath = join(root, "daemon.yaml");
352
+ const lockPath = runtimeArtifactPath(configPath, "start.lock");
353
+ writeFileSync(lockPath, staleOwner);
354
+ const old = new Date(0);
355
+ utimesSync(lockPath, old, old);
356
+ let release;
357
+ const acquiring = acquireRuntimeStartLock(configPath);
358
+ t.after(async () => {
359
+ if (!release) {
360
+ rmSync(lockPath, { force: true });
361
+ release = await acquiring.catch(() => undefined);
362
+ }
363
+ release?.();
364
+ rmSync(root, { recursive: true, force: true });
365
+ });
366
+ release = await Promise.race([
367
+ acquiring,
368
+ new Promise((_resolve, reject) => {
369
+ setTimeout(() => reject(new Error("stale start lock was not recovered promptly")), 500);
370
+ }),
47
371
  ]);
48
- assert.notEqual(first.pid, second.pid);
49
- assert.ok(first.pid);
50
- assert.equal(processIsRunning(first.pid), false);
51
- assert.equal(backgroundRuntimeStatus(configPath).pid, second.pid);
372
+ release();
373
+ assert.equal(existsSync(lockPath), false);
374
+ });
375
+ }
376
+ test("stop waits for the detached runtime to exit before removing its state", { timeout: 5_000 }, async (t) => {
377
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-stop-"));
378
+ const configPath = join(root, "daemon.yaml");
379
+ const statePath = join(root, "cloud-runtime.json");
380
+ const binaryPath = join(root, "gsd-cloud.js");
381
+ writeFileSync(binaryPath, [
382
+ "process.on('SIGTERM',()=>setTimeout(()=>process.exit(0),200));",
383
+ "process.send?.('ready');",
384
+ "setInterval(()=>{},1000);",
385
+ ].join("\n"));
386
+ const child = spawn(process.execPath, [
387
+ binaryPath,
388
+ "_run",
389
+ "--config",
390
+ configPath,
391
+ ], { stdio: ["ignore", "ignore", "ignore", "ipc"] });
392
+ t.after(() => {
393
+ child.kill("SIGKILL");
394
+ rmSync(root, { recursive: true, force: true });
395
+ });
396
+ await new Promise((resolve, reject) => {
397
+ child.once("error", reject);
398
+ child.once("message", () => resolve());
399
+ });
400
+ assert.ok(child.pid);
401
+ writeRuntimeState(configPath, child.pid, [root]);
402
+ const startedAt = Date.now();
403
+ assert.equal(await stopBackgroundRuntime(configPath), true);
404
+ assert.ok(Date.now() - startedAt >= 150);
405
+ assert.equal(existsSync(statePath), false);
406
+ });
407
+ test("background startup terminates its child when state registration fails", { timeout: 5_000 }, async (t) => {
408
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-registration-failure-"));
409
+ const configPath = join(root, "daemon.yaml");
410
+ const pidPath = join(root, "runtime-pid.txt");
411
+ const binaryPath = join(root, "runtime.mjs");
412
+ writeFileSync(binaryPath, [
413
+ 'import { writeFileSync } from "node:fs";',
414
+ `writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`,
415
+ 'process.on("SIGTERM", () => process.exit(0));',
416
+ 'setInterval(() => undefined, 1_000);',
417
+ ].join("\n"));
418
+ mkdirSync(runtimeStatePath(configPath));
419
+ t.after(() => {
420
+ if (existsSync(pidPath)) {
421
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8"), 10);
422
+ if (processIsRunning(pid))
423
+ process.kill(pid, "SIGKILL");
424
+ }
425
+ rmSync(root, { recursive: true, force: true });
426
+ });
427
+ await assert.rejects(startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] }));
428
+ await new Promise((resolve) => setTimeout(resolve, 250));
429
+ if (existsSync(pidPath)) {
430
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8"), 10);
431
+ await waitForCondition(() => !processIsRunning(pid));
432
+ assert.equal(processIsRunning(pid), false);
52
433
  }
53
- finally {
54
- await stopBackgroundRuntime(configPath).catch(() => undefined);
434
+ });
435
+ test("background startup force-stops its child when identity registration fails", { timeout: 8_000 }, async (t) => {
436
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-registration-force-stop-"));
437
+ const configPath = join(root, "daemon.yaml");
438
+ const pidPath = join(root, "runtime-pid.txt");
439
+ const binaryPath = join(root, "runtime.mjs");
440
+ writeFileSync(binaryPath, [
441
+ 'import { writeFileSync } from "node:fs";',
442
+ 'process.on("SIGTERM", () => undefined);',
443
+ `writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`,
444
+ 'setInterval(() => undefined, 1_000);',
445
+ ].join("\n"));
446
+ mkdirSync(runtimeStatePath(configPath));
447
+ t.after(() => {
448
+ if (existsSync(pidPath)) {
449
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8"), 10);
450
+ if (processIsRunning(pid))
451
+ process.kill(pid, "SIGKILL");
452
+ }
55
453
  rmSync(root, { recursive: true, force: true });
454
+ });
455
+ await assert.rejects(startBackgroundRuntime({
456
+ binaryPath,
457
+ configPath,
458
+ projectDirs: [root],
459
+ processIdentityReader: () => {
460
+ const deadline = Date.now() + 1_000;
461
+ while (!existsSync(pidPath) && Date.now() < deadline) {
462
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
463
+ }
464
+ return null;
465
+ },
466
+ }));
467
+ if (existsSync(pidPath)) {
468
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8"), 10);
469
+ await waitForCondition(() => !processIsRunning(pid), 7_000);
470
+ assert.equal(processIsRunning(pid), false);
56
471
  }
57
472
  });
58
- test("verbose background starts forward the flag to the runtime child", { timeout: 10_000 }, async () => {
473
+ test("concurrent starts serialize and leave only the newest runtime running", { timeout: 15_000 }, async (t) => {
474
+ const root = mkdtempSync(join(tmpdir(), "gsd-cloud-concurrent-start-"));
475
+ const configPath = join(root, "daemon.yaml");
476
+ const binaryPath = writeReadyRuntime(root);
477
+ t.after(async () => {
478
+ await stopBackgroundRuntime(configPath).catch(() => undefined);
479
+ rmSync(root, { recursive: true, force: true });
480
+ });
481
+ const [first, second] = await Promise.all([
482
+ startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] }),
483
+ startBackgroundRuntime({ binaryPath, configPath, projectDirs: [root] }),
484
+ ]);
485
+ assert.notEqual(first.pid, second.pid);
486
+ assert.ok(first.pid);
487
+ assert.equal(processIsRunning(first.pid), false);
488
+ assert.equal(backgroundRuntimeStatus(configPath).pid, second.pid);
489
+ });
490
+ test("verbose background starts forward the flag to the runtime child", { timeout: 10_000 }, async (t) => {
59
491
  const root = mkdtempSync(join(tmpdir(), "gsd-cloud-verbose-start-"));
60
492
  const configPath = join(root, "daemon.yaml");
61
493
  const binaryPath = writeReadyRuntime(root);
62
- try {
63
- await startBackgroundRuntime({
64
- binaryPath,
65
- configPath,
66
- projectDirs: [root],
67
- verbose: true,
68
- });
69
- const args = JSON.parse(readFileSync(join(root, "runtime-args.json"), "utf8"));
70
- assert.ok(args.includes("--verbose"));
71
- }
72
- finally {
494
+ t.after(async () => {
73
495
  await stopBackgroundRuntime(configPath).catch(() => undefined);
74
496
  rmSync(root, { recursive: true, force: true });
75
- }
497
+ });
498
+ await startBackgroundRuntime({
499
+ binaryPath,
500
+ configPath,
501
+ projectDirs: [root],
502
+ verbose: true,
503
+ });
504
+ const args = JSON.parse(readFileSync(join(root, "runtime-args.json"), "utf8"));
505
+ assert.ok(args.includes("--verbose"));
76
506
  });
77
- function writeReadyRuntime(root) {
78
- const binaryPath = join(root, "runtime.mjs");
507
+ function writeReadyRuntime(root, readyDelayMs = 0) {
508
+ const binaryPath = join(root, "gsd-cloud.js");
79
509
  writeFileSync(binaryPath, [
80
510
  'import { writeFileSync } from "node:fs";',
81
511
  `writeFileSync(${JSON.stringify(join(root, "runtime-args.json"))}, JSON.stringify(process.argv.slice(2)));`,
82
512
  'process.on("SIGTERM", () => process.exit(0));',
83
- 'process.send?.({ type: "ready" });',
513
+ `setTimeout(() => process.send?.({ type: "ready" }), ${readyDelayMs});`,
84
514
  'setInterval(() => undefined, 1_000);',
85
515
  ].join("\n"));
86
516
  return binaryPath;
87
517
  }
518
+ function writeLegacyRuntime(root, name) {
519
+ const binaryPath = join(root, name);
520
+ writeFileSync(binaryPath, [
521
+ "process.on('SIGTERM', () => process.exit(0));",
522
+ "setInterval(() => undefined, 1_000);",
523
+ ].join("\n"));
524
+ return binaryPath;
525
+ }
88
526
  function processIsRunning(pid) {
89
527
  try {
90
528
  process.kill(pid, 0);
@@ -94,4 +532,12 @@ function processIsRunning(pid) {
94
532
  return false;
95
533
  }
96
534
  }
535
+ async function waitForCondition(condition, timeoutMs = 2_000) {
536
+ const deadline = Date.now() + timeoutMs;
537
+ while (!condition()) {
538
+ if (Date.now() >= deadline)
539
+ throw new Error("timed out waiting for test condition");
540
+ await new Promise((resolve) => setTimeout(resolve, 25));
541
+ }
542
+ }
97
543
  //# sourceMappingURL=runtime-process.test.js.map