@deksden-com/dd-flow-cli 0.9.0-beta.0 → 0.9.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +2 -2
- package/dist/cli/run-cli.js +105 -7
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/cleanup.js +18 -8
- package/dist/services/code-checks.js +194 -44
- package/dist/services/eval-snapshots.js +5 -2
- package/dist/services/hooks.js +25 -22
- package/dist/services/lanes.js +1 -0
- package/dist/services/managed-processes.js +169 -0
- package/dist/services/merge-server.js +5 -0
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/runs.js +50 -6
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +26 -11
- package/dist/services/stage-pause.js +31 -16
- package/dist/services/usage.js +81 -57
- package/dist/services/vnext-code-review.js +46 -12
- package/dist/services/vnext-code.js +59 -22
- package/dist/services/vnext-merge.js +102 -42
- package/dist/services/vnext-plan-review.js +31 -13
- package/dist/services/vnext-plan.js +57 -9
- package/dist/services/work-registry.js +130 -27
- package/dist/storage/database.js +125 -2
- package/package.json +12 -12
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
package/dist/storage/database.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { ensureDir } from "./paths.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const { DatabaseSync } = require("node:sqlite");
|
|
9
|
+
const resourceDatabases = new Map();
|
|
8
10
|
/** Creates the single Work/Session authority used by a fresh vNext beta runtime. */
|
|
9
11
|
export function ensureVnextWorkStorage(db) {
|
|
10
12
|
const legacy = db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('vnext_works', 'vnext_agent_turns', 'flow_agent_turns') LIMIT 1");
|
|
@@ -94,7 +96,7 @@ export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
|
94
96
|
// Internal additive schema changes must be available to every write command.
|
|
95
97
|
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
96
98
|
if (mode !== "read_existing")
|
|
97
|
-
migrate(db);
|
|
99
|
+
migrate(db, dbPath);
|
|
98
100
|
if (mode === "read_existing")
|
|
99
101
|
db.exec("PRAGMA query_only = ON");
|
|
100
102
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -108,6 +110,72 @@ export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
|
108
110
|
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
109
111
|
};
|
|
110
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Opens the small host-wide resource registry. It is intentionally separate
|
|
115
|
+
* from a RUN database: a process or a port can outlive the CLI client that
|
|
116
|
+
* created it, while a RUN database belongs to one flow home.
|
|
117
|
+
*/
|
|
118
|
+
export function getResourceDatabase(resourceHome) {
|
|
119
|
+
ensureDir(resourceHome);
|
|
120
|
+
const dbPath = path.join(resourceHome, "runtime.sqlite");
|
|
121
|
+
const cached = resourceDatabases.get(dbPath);
|
|
122
|
+
if (cached)
|
|
123
|
+
return cached;
|
|
124
|
+
const db = new DatabaseSync(dbPath);
|
|
125
|
+
configureDatabase(db, "initialize");
|
|
126
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
127
|
+
db.exec(`
|
|
128
|
+
CREATE TABLE IF NOT EXISTS managed_processes (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
kind TEXT NOT NULL,
|
|
131
|
+
pid INTEGER,
|
|
132
|
+
pid_started_at TEXT,
|
|
133
|
+
owner_id TEXT NOT NULL,
|
|
134
|
+
lease_token TEXT NOT NULL,
|
|
135
|
+
lease_expires_at TEXT NOT NULL,
|
|
136
|
+
project_id TEXT,
|
|
137
|
+
run_id TEXT,
|
|
138
|
+
work_id TEXT,
|
|
139
|
+
check_id TEXT,
|
|
140
|
+
operation_id TEXT,
|
|
141
|
+
stdout_path TEXT,
|
|
142
|
+
stderr_path TEXT,
|
|
143
|
+
state TEXT NOT NULL,
|
|
144
|
+
started_at TEXT NOT NULL,
|
|
145
|
+
updated_at TEXT NOT NULL,
|
|
146
|
+
finished_at TEXT,
|
|
147
|
+
termination_reason TEXT,
|
|
148
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
149
|
+
);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_managed_processes_lease
|
|
151
|
+
ON managed_processes(state, lease_expires_at, updated_at);
|
|
152
|
+
CREATE TABLE IF NOT EXISTS managed_resources (
|
|
153
|
+
resource_kind TEXT NOT NULL,
|
|
154
|
+
resource_key TEXT NOT NULL,
|
|
155
|
+
owner_id TEXT NOT NULL,
|
|
156
|
+
lease_token TEXT NOT NULL,
|
|
157
|
+
lease_expires_at TEXT NOT NULL,
|
|
158
|
+
process_id TEXT,
|
|
159
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
160
|
+
created_at TEXT NOT NULL,
|
|
161
|
+
updated_at TEXT NOT NULL,
|
|
162
|
+
PRIMARY KEY(resource_kind, resource_key)
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_managed_resources_lease
|
|
165
|
+
ON managed_resources(lease_expires_at, updated_at);
|
|
166
|
+
`);
|
|
167
|
+
const database = {
|
|
168
|
+
path: dbPath,
|
|
169
|
+
writable: true,
|
|
170
|
+
close: () => { resourceDatabases.delete(dbPath); db.close?.(); },
|
|
171
|
+
exec: (sql) => db.exec(sql),
|
|
172
|
+
run: (sql, params = []) => db.prepare(sql).run(...params),
|
|
173
|
+
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
174
|
+
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
175
|
+
};
|
|
176
|
+
resourceDatabases.set(dbPath, database);
|
|
177
|
+
return database;
|
|
178
|
+
}
|
|
111
179
|
function emptyReadOnlyDatabase(dbPath) {
|
|
112
180
|
return {
|
|
113
181
|
path: dbPath,
|
|
@@ -125,7 +193,7 @@ function configureDatabase(db, mode) {
|
|
|
125
193
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
126
194
|
}
|
|
127
195
|
}
|
|
128
|
-
function migrate(db) {
|
|
196
|
+
function migrate(db, dbPath) {
|
|
129
197
|
db.exec(`
|
|
130
198
|
CREATE TABLE IF NOT EXISTS projects (
|
|
131
199
|
id TEXT PRIMARY KEY,
|
|
@@ -351,6 +419,7 @@ function migrate(db) {
|
|
|
351
419
|
project_id TEXT NOT NULL,
|
|
352
420
|
harness TEXT NOT NULL DEFAULT 'codex-desktop',
|
|
353
421
|
provider_session_id TEXT,
|
|
422
|
+
provider_parent_session_id TEXT,
|
|
354
423
|
agent_id TEXT,
|
|
355
424
|
parent_id TEXT,
|
|
356
425
|
provider TEXT,
|
|
@@ -713,6 +782,7 @@ function migrate(db) {
|
|
|
713
782
|
ensureColumn(db, "sessions", "metadata_json", "ALTER TABLE sessions ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'");
|
|
714
783
|
ensureColumn(db, "sessions", "run_id", "ALTER TABLE sessions ADD COLUMN run_id TEXT");
|
|
715
784
|
ensureColumn(db, "sessions", "provider_session_id", "ALTER TABLE sessions ADD COLUMN provider_session_id TEXT");
|
|
785
|
+
ensureColumn(db, "sessions", "provider_parent_session_id", "ALTER TABLE sessions ADD COLUMN provider_parent_session_id TEXT");
|
|
716
786
|
ensureColumn(db, "sessions", "harness", "ALTER TABLE sessions ADD COLUMN harness TEXT NOT NULL DEFAULT 'codex-desktop'");
|
|
717
787
|
ensureColumn(db, "sessions", "agent_id", "ALTER TABLE sessions ADD COLUMN agent_id TEXT");
|
|
718
788
|
ensureColumn(db, "sessions", "provider", "ALTER TABLE sessions ADD COLUMN provider TEXT");
|
|
@@ -727,6 +797,7 @@ function migrate(db) {
|
|
|
727
797
|
ensureColumn(db, "sessions", "plan_item_id", "ALTER TABLE sessions ADD COLUMN plan_item_id TEXT");
|
|
728
798
|
ensureColumn(db, "sessions", "session_kind", "ALTER TABLE sessions ADD COLUMN session_kind TEXT");
|
|
729
799
|
ensureColumn(db, "sessions", "coverage_units_json", "ALTER TABLE sessions ADD COLUMN coverage_units_json TEXT NOT NULL DEFAULT '[]'");
|
|
800
|
+
migrateSessionStorageKeys(db, dbPath);
|
|
730
801
|
ensureColumn(db, "usage", "cache_read_input_tokens", "ALTER TABLE usage ADD COLUMN cache_read_input_tokens INTEGER");
|
|
731
802
|
ensureColumn(db, "usage", "cache_write_input_tokens", "ALTER TABLE usage ADD COLUMN cache_write_input_tokens INTEGER");
|
|
732
803
|
ensureColumn(db, "usage", "uncached_input_tokens", "ALTER TABLE usage ADD COLUMN uncached_input_tokens INTEGER");
|
|
@@ -783,6 +854,58 @@ function migrate(db) {
|
|
|
783
854
|
ON runs(short_id);
|
|
784
855
|
`);
|
|
785
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Older builds used public or `harness:native` strings as foreign keys. Move
|
|
859
|
+
* those keys once to an opaque storage key while preserving the original
|
|
860
|
+
* native value in provider_session_id. We deliberately never split a native
|
|
861
|
+
* value on `:`: rows without a recorded provider ID remain untouched and are
|
|
862
|
+
* reported by normal preflight instead of guessed.
|
|
863
|
+
*/
|
|
864
|
+
function migrateSessionStorageKeys(db, dbPath) {
|
|
865
|
+
const rows = db.prepare("SELECT project_id, session_id, harness, provider_session_id, parent_session_id FROM sessions WHERE provider_session_id IS NOT NULL").all();
|
|
866
|
+
const changed = rows.map((row) => ({ ...row, next: storageSessionKey(row.harness, row.provider_session_id) })).filter((row) => row.session_id !== row.next);
|
|
867
|
+
if (changed.length === 0)
|
|
868
|
+
return;
|
|
869
|
+
// This is the only non-additive storage migration. `VACUUM INTO` captures
|
|
870
|
+
// a consistent SQLite snapshot (including WAL state) before keys change.
|
|
871
|
+
if (dbPath && fs.existsSync(dbPath)) {
|
|
872
|
+
const backup = `${dbPath}.pre-session-key-v1-${Date.now()}.sqlite`;
|
|
873
|
+
db.exec(`VACUUM INTO '${backup.replace(/'/g, "''")}'`);
|
|
874
|
+
}
|
|
875
|
+
const duplicate = new Set();
|
|
876
|
+
for (const row of changed) {
|
|
877
|
+
const key = `${row.project_id}\0${row.next}`;
|
|
878
|
+
if (duplicate.has(key) || db.prepare("SELECT 1 FROM sessions WHERE project_id = ? AND session_id = ? AND session_id <> ?").get(row.project_id, row.next, row.session_id)) {
|
|
879
|
+
throw new AppError("session_identity_migration_conflict", "Cannot migrate ambiguous harness/native Session identity", 1, { project_id: row.project_id, harness_id: row.harness, session_id: row.provider_session_id });
|
|
880
|
+
}
|
|
881
|
+
duplicate.add(key);
|
|
882
|
+
}
|
|
883
|
+
db.exec("BEGIN IMMEDIATE");
|
|
884
|
+
try {
|
|
885
|
+
// First update references. Session rows themselves are updated afterwards,
|
|
886
|
+
// so every dependent row remains resolvable throughout the transaction.
|
|
887
|
+
for (const row of changed) {
|
|
888
|
+
db.prepare("UPDATE work_sessions SET session_id = ? WHERE session_id = ? AND work_id IN (SELECT work_id FROM works WHERE project_id = ?)").run(row.next, row.session_id, row.project_id);
|
|
889
|
+
db.prepare("UPDATE flow_session_segments SET session_id = ? WHERE project_id = ? AND session_id = ?").run(row.next, row.project_id, row.session_id);
|
|
890
|
+
db.prepare("UPDATE usage SET session_id = ? WHERE project_id = ? AND session_id = ?").run(row.next, row.project_id, row.session_id);
|
|
891
|
+
const parameters = [row.next, row.project_id, row.session_id];
|
|
892
|
+
db.prepare("UPDATE hook_events SET session_id = ? WHERE project_id = ? AND session_id = ?").run(...parameters);
|
|
893
|
+
db.prepare("UPDATE flow_jobs SET worker_session_id = ? WHERE project_id = ? AND worker_session_id = ?").run(...parameters);
|
|
894
|
+
db.prepare("UPDATE merge_queue SET claimed_by_session_id = ? WHERE project_id = ? AND claimed_by_session_id = ?").run(...parameters);
|
|
895
|
+
db.prepare("UPDATE sessions SET parent_session_id = ? WHERE project_id = ? AND parent_session_id = ?").run(...parameters);
|
|
896
|
+
db.prepare("UPDATE sessions SET session_id = ? WHERE project_id = ? AND session_id = ?").run(...parameters);
|
|
897
|
+
}
|
|
898
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_harness_native ON sessions(project_id, harness, provider_session_id) WHERE provider_session_id IS NOT NULL");
|
|
899
|
+
db.exec("COMMIT");
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
db.exec("ROLLBACK");
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function storageSessionKey(harness, native) {
|
|
907
|
+
return `SES-${crypto.createHash("sha256").update(harness).update("\0").update(native).digest("hex").slice(0, 24)}`;
|
|
908
|
+
}
|
|
786
909
|
function migrateProjectScopedIdentity(db) {
|
|
787
910
|
const protocolColumns = db.prepare("PRAGMA table_info(protocols)").all();
|
|
788
911
|
const legacyGlobalPrimaryKey = protocolColumns.some((column) => column.name === "id" && column.pk === 1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.7",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
"node": ">=26.0.0",
|
|
17
17
|
"pnpm": ">=10.0.0"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "eslint . --max-warnings=0",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"changeset": "changeset",
|
|
25
|
+
"version-packages": "changeset version",
|
|
26
|
+
"release": "pnpm run build && changeset publish"
|
|
27
|
+
},
|
|
19
28
|
"devDependencies": {
|
|
20
29
|
"@changesets/cli": "^2.31.0",
|
|
21
30
|
"@eslint/js": "^9.39.1",
|
|
@@ -34,14 +43,5 @@
|
|
|
34
43
|
"publishConfig": {
|
|
35
44
|
"access": "public"
|
|
36
45
|
},
|
|
37
|
-
"license": "MIT"
|
|
38
|
-
|
|
39
|
-
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
40
|
-
"typecheck": "tsc --noEmit",
|
|
41
|
-
"lint": "eslint . --max-warnings=0",
|
|
42
|
-
"test": "vitest run",
|
|
43
|
-
"changeset": "changeset",
|
|
44
|
-
"version-packages": "changeset version",
|
|
45
|
-
"release": "pnpm run build && changeset publish"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Adversarial acceptance checks, independent of the long CLI fixture suite.
|
|
2
|
+
// Run after building: pnpm build && node tools/audit-runtime-fix-boundaries.mjs
|
|
3
|
+
// Uses only disposable local workspaces; no provider or production DB calls.
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { log } from "node:console";
|
|
10
|
+
import { setTimeout, clearTimeout } from "node:timers";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { once } from "node:events";
|
|
13
|
+
import { createContext } from "../dist/runtime/context.js";
|
|
14
|
+
import { getResourceDatabase } from "../dist/storage/database.js";
|
|
15
|
+
import { checksForRunAt, runCodeChecks, checkReceipts } from "../dist/services/code-checks.js";
|
|
16
|
+
import { claimExpiredManagedProcesses, confirmManagedProcess, finishManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess, reservePorts } from "../dist/services/managed-processes.js";
|
|
17
|
+
import { selectRepairChecks } from "../dist/services/vnext-code.js";
|
|
18
|
+
|
|
19
|
+
const results = [];
|
|
20
|
+
const observations = {};
|
|
21
|
+
async function check(name, action) {
|
|
22
|
+
try { await action(); results.push({ name, passed: true }); }
|
|
23
|
+
catch (error) { results.push({ name, passed: false, expected: error.expected, actual: error.actual, message: error.message }); }
|
|
24
|
+
}
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "dd-flow-fix-audit-"));
|
|
26
|
+
const resources = path.join(root, "resources");
|
|
27
|
+
const context = createContext({ DD_FLOW_HOME: path.join(root, "flow"), DD_FLOW_RESOURCE_HOME: resources });
|
|
28
|
+
const projectRoot = path.join(root, "project");
|
|
29
|
+
fs.mkdirSync(projectRoot);
|
|
30
|
+
try {
|
|
31
|
+
const request = {
|
|
32
|
+
projectId: "PRJ-AUDIT", runId: "RUN-AUDIT", runHome: path.join(root, "run"), workspaceRoot: projectRoot, scope: "aggregate",
|
|
33
|
+
checks: [{ id: "CHK-AUDIT", command: "sleep 0.05", purpose: "concurrent claim audit", run_at: "code", availability: "available" }]
|
|
34
|
+
};
|
|
35
|
+
const calls = await Promise.allSettled([runCodeChecks(context, request), runCodeChecks(context, request)]);
|
|
36
|
+
const rows = checkReceipts(context, { projectId: request.projectId, runId: request.runId });
|
|
37
|
+
observations.concurrent = calls.map((call) => call.status === "fulfilled" ? { status: call.status, receipts: call.value.map(({ id, status }) => ({ id, status })) } : { status: call.status, code: call.reason.code });
|
|
38
|
+
await check("a concurrent caller receives check_in_progress during the spawn window", () => assert.equal(calls.filter((call) => call.status === "rejected" && call.reason.code === "check_in_progress").length, 1));
|
|
39
|
+
await check("returned and persisted terminal check statuses agree", () => {
|
|
40
|
+
for (const call of calls.filter((call) => call.status === "fulfilled")) {
|
|
41
|
+
for (const receipt of call.value) assert.equal(rows.find((row) => row.id === receipt.id)?.status, receipt.status);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const source = { id: "CHK-QUALITY", command: "true", purpose: "aggregate quality gate", run_at: "code", availability: "available" };
|
|
46
|
+
await check("semantic repair retains an aggregate gate for context but does not run it in a Work", () => assert.deepEqual(checksForRunAt(selectRepairChecks({ semanticChecks: [source] }), "work"), []));
|
|
47
|
+
|
|
48
|
+
const record = registerManagedProcess(context, { kind: "audit", ownerId: "audit-owner", leaseMs: -1 });
|
|
49
|
+
const claimed = claimExpiredManagedProcesses(context, "interrupted-reconciler");
|
|
50
|
+
await check("orphan cleanup can be reclaimed after a reconciler interruption", () => {
|
|
51
|
+
assert.ok(claimed.some((item) => item.id === record.id));
|
|
52
|
+
context.now = () => "2099-01-01T00:00:00.000Z";
|
|
53
|
+
assert.ok(claimExpiredManagedProcesses(context, "next-reconciler").some((item) => item.id === record.id));
|
|
54
|
+
});
|
|
55
|
+
context.now = () => new Date().toISOString();
|
|
56
|
+
const ownedProcess = registerManagedProcess(context, { kind: "audit", ownerId: "audit-with-port" });
|
|
57
|
+
const allocation = await reservePorts(context, { ownerId: ownedProcess.owner_id, processId: ownedProcess.id, names: ["api"] });
|
|
58
|
+
try {
|
|
59
|
+
finishManagedProcess(context, { id: ownedProcess.id, leaseToken: ownedProcess.lease_token, state: "stopped" });
|
|
60
|
+
await check("terminal process cleanup also releases its durable port claims", () => {
|
|
61
|
+
const rows = getResourceDatabase(resources).all("SELECT * FROM managed_resources WHERE process_id = ?", [ownedProcess.id]);
|
|
62
|
+
assert.equal(rows.length, 0);
|
|
63
|
+
});
|
|
64
|
+
} finally { allocation.release(); }
|
|
65
|
+
await check("orphan reconciliation also terminates the process-owned child tree", async () => {
|
|
66
|
+
// Both processes belong only to this temporary test. A private process
|
|
67
|
+
// group makes the finally block able to clean up even a failed assertion.
|
|
68
|
+
const tree = spawn(process.execPath, ["-e", `const {spawn}=require('node:child_process'); const child=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'}); console.log(child.pid); setInterval(()=>{},1000);`], { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
69
|
+
const closed = once(tree, "close");
|
|
70
|
+
let timer;
|
|
71
|
+
try {
|
|
72
|
+
const data = await Promise.race([once(tree.stdout, "data"), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("test process did not start")), 2000); })]);
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
const childPid = Number(String(data[0]).trim());
|
|
75
|
+
assert.ok(Number.isInteger(childPid) && childPid > 0);
|
|
76
|
+
const record = registerManagedProcess(context, { kind: "audit-tree", ownerId: "audit-tree-owner" });
|
|
77
|
+
confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: tree.pid, processGroupId: tree.pid });
|
|
78
|
+
getResourceDatabase(resources).run("UPDATE managed_processes SET lease_expires_at = ? WHERE id = ?", ["2000-01-01T00:00:00.000Z", record.id]);
|
|
79
|
+
await reconcileExpiredManagedProcesses(context, "audit-tree-cleanup", 20);
|
|
80
|
+
let alive = true;
|
|
81
|
+
try { process.kill(childPid, 0); } catch { alive = false; }
|
|
82
|
+
assert.equal(alive, false);
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
try { process.kill(-tree.pid, "SIGKILL"); } catch { /* our group has already stopped */ }
|
|
86
|
+
await closed;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
await check("the check owner reaches a terminal process state on ordinary completion", () => assert.ok(managedProcessStatus(context).filter((item) => item.kind === "check").every((item) => ["stopped", "failed"].includes(item.state))));
|
|
90
|
+
} finally {
|
|
91
|
+
context.db.close();
|
|
92
|
+
getResourceDatabase(resources).close();
|
|
93
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
log(JSON.stringify({ checks: results.length, passed: results.filter((item) => item.passed).length, results, observations }, null, 2));
|
|
96
|
+
process.exitCode = results.some((item) => !item.passed) ? 1 : 0;
|