@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.
- package/README.md +10 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +25 -12
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
- package/dist/resources/extensions/gsd/db/engine.js +8 -2
- package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
- package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
- package/dist/resources/extensions/gsd/model-router.js +10 -5
- package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
- package/dist/resources/extensions/gsd/preferences-models.js +50 -10
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
- package/dist/resources/extensions/gsd/preferences.js +7 -0
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
- package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
- package/dist/resources/extensions/subagent/index.js +1 -1
- package/dist/startup-model-validation.d.ts +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +7 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/rpc.d.ts +1 -1
- package/packages/contracts/dist/rpc.d.ts.map +1 -1
- package/packages/contracts/dist/rpc.js +1 -1
- package/packages/contracts/dist/rpc.js.map +1 -1
- package/packages/contracts/dist/rpc.test.js +1 -1
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
- package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +12 -2
- package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +36 -12
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
- package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.js +429 -61
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +2 -2
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +113 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +84 -9
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js +18 -6
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +9 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts +1 -1
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +1 -0
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +46 -12
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
- package/src/resources/extensions/gsd/db/engine.ts +9 -1
- package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
- package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
- package/src/resources/extensions/gsd/preferences-models.ts +54 -10
- package/src/resources/extensions/gsd/preferences-types.ts +10 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
- package/src/resources/extensions/gsd/preferences.ts +13 -0
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
- package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +1 -1
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
- /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("
|
|
15
|
-
const root = mkdtempSync(join(tmpdir(), "gsd-cloud-
|
|
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
|
-
"-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assert.
|
|
32
|
-
assert.equal(
|
|
33
|
-
|
|
34
|
-
|
|
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("
|
|
40
|
-
const root = mkdtempSync(join(tmpdir(), "gsd-cloud-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
assert.
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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("
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|