rollbridge 0.1.13 → 0.1.15
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 +6 -5
- package/compose.yml +17 -0
- package/docs/cli.md +28 -8
- package/docs/troubleshooting.md +18 -0
- package/package.json +1 -1
- package/src/cli.js +57 -18
- package/src/daemon-runtime.js +316 -0
- package/src/daemon.js +25 -4
- package/test/completion.test.js +1 -0
- package/test/daemon-bootstrap.test.js +40 -7
- package/test/daemon-runtime.test.js +90 -0
- package/test/fixtures/dummy-app.js +1 -1
- package/test/package-metadata.test.js +39 -0
- package/test/release-runtime-retention.test.js +357 -0
- package/test/rollbridge.test.js +3 -1
- package/tmp/worker-control/rollbridge-bootstrap/activity-3.jsonl +0 -28
- package/tmp/worker-control/rollbridge-bootstrap/current-head-review-activity.jsonl +0 -13
- package/tmp/worker-control/rollbridge-bootstrap/current-head-review-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/fs-probe-10.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/plan.md +0 -9
- package/tmp/worker-control/rollbridge-bootstrap/repair-activity.jsonl +0 -3
- package/tmp/worker-control/rollbridge-bootstrap/repair-fresh-activity.jsonl +0 -25
- package/tmp/worker-control/rollbridge-bootstrap/repair-fresh-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/repair-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/review-activity.jsonl +0 -14
- package/tmp/worker-control/rollbridge-bootstrap/review-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/terminal-repair-activity.jsonl +0 -3
- package/tmp/worker-control/rollbridge-bootstrap/terminal-repair-fresh-activity.jsonl +0 -38
- package/tmp/worker-control/rollbridge-bootstrap/terminal-repair-fresh-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/terminal-repair-transcript.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/transcript-2.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/transcript-3.jsonl +0 -1
- package/tmp/worker-control/rollbridge-bootstrap/transcript.jsonl +0 -1
package/src/daemon.js
CHANGED
|
@@ -18,7 +18,7 @@ const STATE_PERSIST_INTERVAL_MS = 5000
|
|
|
18
18
|
* @typedef {import("./json.js").JsonValue} JsonValue
|
|
19
19
|
* @typedef {{releaseId?: string, releasePath: string, revision?: string}} DeployArgs
|
|
20
20
|
* @typedef {{id: string, process: import("./managed-process.js").ManagedProcessStatus}} ProcessStatus
|
|
21
|
-
* @typedef {{activeReleaseId: string | null, application: string, control: import("./config.js").ControlConfig, orphans: {id: string, pid: number, releaseId: string | null}[], proxy: {host: string, port: number | undefined, upstreamHost: string}, releases: import("./release-group.js").ReleaseStatus[], services: ProcessStatus[], singletons: ProcessStatus[]}} DaemonStatus
|
|
21
|
+
* @typedef {{activeReleaseId: string | null, application: string, control: import("./config.js").ControlConfig, daemonRuntime: import("./daemon-runtime.js").DaemonRuntimeIdentity | undefined, orphans: {id: string, pid: number, releaseId: string | null}[], proxy: {host: string, port: number | undefined, upstreamHost: string}, releases: import("./release-group.js").ReleaseStatus[], services: ProcessStatus[], singletons: ProcessStatus[]}} DaemonStatus
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
export default class RollbridgeDaemon {
|
|
@@ -27,10 +27,12 @@ export default class RollbridgeDaemon {
|
|
|
27
27
|
* @param {import("./config.js").RollbridgeConfig} args.config - Rollbridge config.
|
|
28
28
|
* @param {string} [args.configPath] - Config file path to reload before deploys.
|
|
29
29
|
* @param {(message: string, data?: Record<string, JsonValue>) => void} [args.logger] - Logger.
|
|
30
|
+
* @param {import("./daemon-runtime.js").DaemonRuntimeIdentity} [args.runtime] - Immutable daemon runtime identity.
|
|
30
31
|
*/
|
|
31
|
-
constructor({config, configPath, logger}) {
|
|
32
|
+
constructor({config, configPath, logger, runtime}) {
|
|
32
33
|
this.config = config
|
|
33
34
|
this.configPath = configPath
|
|
35
|
+
this.runtime = runtime
|
|
34
36
|
this.eventLog = new EventLog(EVENT_HISTORY_LIMIT)
|
|
35
37
|
|
|
36
38
|
const baseLogger = logger || ((message, data = {}) => console.log(JSON.stringify({at: new Date().toISOString(), data, message})))
|
|
@@ -64,11 +66,29 @@ export default class RollbridgeDaemon {
|
|
|
64
66
|
this.proxy.on("error", (error, req, res) => this.onProxyError(error, req, res))
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Starts daemon listeners.
|
|
71
|
+
* @param {{exposeControl?: boolean}} [options] - Whether to expose the control socket immediately.
|
|
72
|
+
* @returns {Promise<void>} Resolves when the requested listeners are ready.
|
|
73
|
+
*/
|
|
74
|
+
async start({exposeControl = true} = {}) {
|
|
69
75
|
await this.reportOrphans()
|
|
70
76
|
await this.startProxy()
|
|
77
|
+
if (exposeControl) await this.exposeControl()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @returns {Promise<void>} Exposes control commands and begins periodic state persistence. */
|
|
81
|
+
async exposeControl() {
|
|
82
|
+
if (this.stopping) throw new Error("Rollbridge is shutting down")
|
|
83
|
+
|
|
71
84
|
await this.startControlServer()
|
|
85
|
+
|
|
86
|
+
if (this.stopping) {
|
|
87
|
+
await this.closeServer(this.controlServer)
|
|
88
|
+
await fs.rm(this.config.control.path, {force: true})
|
|
89
|
+
throw new Error("Rollbridge is shutting down")
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
this.startStatePersistence()
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -800,6 +820,7 @@ export default class RollbridgeDaemon {
|
|
|
800
820
|
activeReleaseId: this.activeRelease ? this.activeRelease.releaseId : null,
|
|
801
821
|
application: this.config.application,
|
|
802
822
|
control: {...this.config.control},
|
|
823
|
+
daemonRuntime: this.runtime ? {...this.runtime} : undefined,
|
|
803
824
|
orphans: [...this.orphans],
|
|
804
825
|
proxy: {
|
|
805
826
|
host: this.config.proxy.host,
|
package/test/completion.test.js
CHANGED
|
@@ -44,6 +44,7 @@ test("completion bash prints a sourceable script with commands and option flags"
|
|
|
44
44
|
assert.match(output, /compgen -W "daemon deploy rollback ensure-daemon status stop restart shutdown validate doctor logs events predeploy-cleanup recover completion"/)
|
|
45
45
|
// A command's own options are completed after the command.
|
|
46
46
|
assert.match(output, /deploy\)\n\s+opts="[^"]*--release-path[^"]*"/)
|
|
47
|
+
assert.match(output, /ensure-daemon\)\n\s+opts="[^"]*--daemon-runtime-path[^"]*"/)
|
|
47
48
|
assert.match(output, /restart\)\n\s+opts="[^"]*--policy[^"]*"/)
|
|
48
49
|
})
|
|
49
50
|
|
|
@@ -47,7 +47,7 @@ test("daemon bootstrap activates the exact release through the foreground daemon
|
|
|
47
47
|
const child = spawnDaemon(fixture, {releaseId: "release-42", revision: "abc123"})
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
|
-
await waitForLog(child, "
|
|
50
|
+
await waitForLog(child, "control socket listening")
|
|
51
51
|
const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
|
|
52
52
|
const activeRelease = assertRelease(status, "release-42")
|
|
53
53
|
|
|
@@ -64,6 +64,33 @@ test("daemon bootstrap activates the exact release through the foreground daemon
|
|
|
64
64
|
}
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
+
test("daemon bootstrap does not expose control deploys until activation completes", async () => {
|
|
68
|
+
const fixture = await createFixture({healthGate: true, healthTimeoutMs: 60000})
|
|
69
|
+
const started = waitForFile(fixture.startedPath)
|
|
70
|
+
const child = spawnDaemon(fixture, {releaseId: "bootstrap-release", revision: "bootstrap123"})
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await started
|
|
74
|
+
await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
|
|
75
|
+
|
|
76
|
+
await fs.writeFile(fixture.healthGatePath, "ready\n")
|
|
77
|
+
await waitForLog(child, "control socket listening")
|
|
78
|
+
|
|
79
|
+
const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
|
|
80
|
+
|
|
81
|
+
assert.equal(status.activeReleaseId, "bootstrap-release")
|
|
82
|
+
assert.ok(Array.isArray(status.releases))
|
|
83
|
+
assert.equal(status.releases.length, 1)
|
|
84
|
+
assertRelease(status, "bootstrap-release")
|
|
85
|
+
|
|
86
|
+
child.kill("SIGTERM")
|
|
87
|
+
assert.equal((await once(child, "exit"))[0], 0)
|
|
88
|
+
} finally {
|
|
89
|
+
if (child.exitCode === null) child.kill("SIGKILL")
|
|
90
|
+
await fs.rm(fixture.root, {force: true, recursive: true})
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
67
94
|
test("plain daemon startup remains listener-only with no active release", async () => {
|
|
68
95
|
const fixture = await createFixture()
|
|
69
96
|
const child = spawn(process.execPath, [binPath, "daemon", "--config", fixture.configPath], {stdio: ["pipe", "pipe", "pipe"]})
|
|
@@ -182,7 +209,7 @@ test("daemon bootstrap reports but does not kill a live process from statePath",
|
|
|
182
209
|
const child = spawnDaemon(fixture, {releaseId: "recovered", revision: "def456"})
|
|
183
210
|
|
|
184
211
|
try {
|
|
185
|
-
await waitForLog(child, "
|
|
212
|
+
await waitForLog(child, "control socket listening")
|
|
186
213
|
const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
|
|
187
214
|
|
|
188
215
|
assert.ok(leftover.pid !== undefined && isProcessAlive(leftover.pid))
|
|
@@ -228,10 +255,10 @@ test("failed daemon bootstrap preserves prior live process records in statePath"
|
|
|
228
255
|
})
|
|
229
256
|
|
|
230
257
|
/**
|
|
231
|
-
* @param {{healthPath?: string, healthTimeoutMs?: number, multiProcessSignal?: boolean, persistState?: boolean}} [options] - Fixture options.
|
|
232
|
-
* @returns {Promise<{configPath: string, gatePath: string, lifecyclePath: string, root: string, socketPath: string, startedPath: string, statePath: string, stoppedPath: string}>} Fixture paths.
|
|
258
|
+
* @param {{healthGate?: boolean, healthPath?: string, healthTimeoutMs?: number, multiProcessSignal?: boolean, persistState?: boolean}} [options] - Fixture options.
|
|
259
|
+
* @returns {Promise<{configPath: string, gatePath: string, healthGatePath: string, lifecyclePath: string, root: string, socketPath: string, startedPath: string, statePath: string, stoppedPath: string}>} Fixture paths.
|
|
233
260
|
*/
|
|
234
|
-
async function createFixture({healthPath = "/ping", healthTimeoutMs = 1000, multiProcessSignal = false, persistState = false} = {}) {
|
|
261
|
+
async function createFixture({healthGate = false, healthPath = "/ping", healthTimeoutMs = 1000, multiProcessSignal = false, persistState = false} = {}) {
|
|
235
262
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-bootstrap-"))
|
|
236
263
|
const socketPath = path.join(root, "control.sock")
|
|
237
264
|
const statePath = path.join(root, "state.json")
|
|
@@ -239,9 +266,15 @@ async function createFixture({healthPath = "/ping", healthTimeoutMs = 1000, mult
|
|
|
239
266
|
const stoppedPath = path.join(root, "stopped.pid")
|
|
240
267
|
const lifecyclePath = path.join(root, "lifecycle.jsonl")
|
|
241
268
|
const gatePath = path.join(root, "continue.fifo")
|
|
269
|
+
const healthGatePath = path.join(root, "health-ready")
|
|
242
270
|
const configPath = path.join(root, "rollbridge.js")
|
|
243
271
|
const command = `${JSON.stringify(process.execPath)} ${JSON.stringify(dummyAppPath)}`
|
|
244
272
|
const lifecycleEnv = {ROLLBRIDGE_TEST_LIFECYCLE_PATH: lifecyclePath}
|
|
273
|
+
const webEnv = {
|
|
274
|
+
ROLLBRIDGE_TEST_STARTED_PATH: startedPath,
|
|
275
|
+
ROLLBRIDGE_TEST_STOPPED_PATH: stoppedPath,
|
|
276
|
+
...(healthGate ? {ROLLBRIDGE_TEST_HEALTH_GATE_PATH: healthGatePath} : {})
|
|
277
|
+
}
|
|
245
278
|
const config = {
|
|
246
279
|
application: "bootstrap-test",
|
|
247
280
|
control: {path: socketPath},
|
|
@@ -249,7 +282,7 @@ async function createFixture({healthPath = "/ping", healthTimeoutMs = 1000, mult
|
|
|
249
282
|
{command: `trap '' TERM; printf '%s\\n' '{"event":"shutdown"}' >> ${JSON.stringify(lifecyclePath)}; kill -TERM "$ROLLBRIDGE_TEST_DAEMON_PID"; printf '{"event":"started","pid":%s,"processId":"database","replicaIndex":"0"}\\n' "$$" >> ${JSON.stringify(lifecyclePath)}; read ignored < ${JSON.stringify(gatePath)}`, env: lifecycleEnv, id: "database", policy: "service"},
|
|
250
283
|
{command: `exec ${command}`, env: lifecycleEnv, id: "worker", policy: "companion", replicas: 2},
|
|
251
284
|
{command: `exec ${command}`, env: lifecycleEnv, health: {path: healthPath, timeoutMs: healthTimeoutMs}, id: "web", policy: "proxied", port: {from: 0, to: 0}}
|
|
252
|
-
] : [{command, env:
|
|
285
|
+
] : [{command, env: webEnv, health: {path: healthPath, timeoutMs: healthTimeoutMs}, id: "web", policy: "proxied", port: {from: 0, to: 0}}],
|
|
253
286
|
proxy: {host: "127.0.0.1", port: 0},
|
|
254
287
|
...(persistState ? {statePath} : {})
|
|
255
288
|
}
|
|
@@ -264,7 +297,7 @@ async function createFixture({healthPath = "/ping", healthTimeoutMs = 1000, mult
|
|
|
264
297
|
}
|
|
265
298
|
|
|
266
299
|
await fs.writeFile(configPath, `${setup}module.exports = ${JSON.stringify(config, null, 2)}\n`)
|
|
267
|
-
return {configPath, gatePath, lifecyclePath, root, socketPath, startedPath, statePath, stoppedPath}
|
|
300
|
+
return {configPath, gatePath, healthGatePath, lifecyclePath, root, socketPath, startedPath, statePath, stoppedPath}
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
/**
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict"
|
|
4
|
+
import fs from "node:fs/promises"
|
|
5
|
+
import os from "node:os"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import test from "node:test"
|
|
8
|
+
import {prepareDaemonRuntime} from "../src/daemon-runtime.js"
|
|
9
|
+
|
|
10
|
+
test("concurrent runtime preparation converges on one validated content-addressed snapshot", async () => {
|
|
11
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-concurrent-"))
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const identities = await Promise.all([
|
|
15
|
+
prepareDaemonRuntime(root),
|
|
16
|
+
prepareDaemonRuntime(root),
|
|
17
|
+
prepareDaemonRuntime(root)
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(identities, [identities[0], identities[0], identities[0]])
|
|
21
|
+
assert.match(identities[0].digest, /^[a-f0-9]{64}$/)
|
|
22
|
+
assert.equal(path.dirname(identities[0].path), root)
|
|
23
|
+
assert.equal(JSON.parse(await fs.readFile(path.join(identities[0].path, "runtime.json"), "utf8")).digest, identities[0].digest)
|
|
24
|
+
|
|
25
|
+
const entries = (await fs.readdir(root)).filter((entry) => entry.startsWith(".prepare-"))
|
|
26
|
+
assert.deepEqual(entries, [])
|
|
27
|
+
} finally {
|
|
28
|
+
await fs.rm(root, {force: true, recursive: true})
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("preparation fails closed when an existing content-addressed snapshot is corrupt", async () => {
|
|
33
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-corrupt-"))
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const identity = await prepareDaemonRuntime(root)
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(path.join(identity.path, "src", "daemon.js"), "corrupt\n")
|
|
39
|
+
await assert.rejects(() => prepareDaemonRuntime(root), /runtime validation failed/)
|
|
40
|
+
} finally {
|
|
41
|
+
await fs.rm(root, {force: true, recursive: true})
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("runtime preparation rejects a symlinked or shared-writable parent", async (t) => {
|
|
46
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-permissions-"))
|
|
47
|
+
const target = path.join(root, "target")
|
|
48
|
+
const symlink = path.join(root, "symlink")
|
|
49
|
+
const shared = path.join(root, "shared")
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await fs.mkdir(target)
|
|
53
|
+
await fs.symlink(target, symlink, "dir")
|
|
54
|
+
await assert.rejects(() => prepareDaemonRuntime(symlink), /must be a real directory/)
|
|
55
|
+
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
t.skip("POSIX directory permissions are not available on Windows")
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fs.mkdir(shared, {mode: 0o777})
|
|
62
|
+
await fs.chmod(shared, 0o777)
|
|
63
|
+
await assert.rejects(() => prepareDaemonRuntime(shared), /must not be writable by group or other users/)
|
|
64
|
+
} finally {
|
|
65
|
+
await fs.rm(root, {force: true, recursive: true})
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("runtime preparation rejects a private leaf beneath a replaceable ancestor", async (t) => {
|
|
70
|
+
if (process.platform === "win32") {
|
|
71
|
+
t.skip("POSIX directory permissions are not available on Windows")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const unsafeAncestor = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-unsafe-ancestor-"))
|
|
76
|
+
const privateLeaf = path.join(unsafeAncestor, "private-runtime")
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await fs.chmod(unsafeAncestor, 0o777)
|
|
80
|
+
await fs.mkdir(privateLeaf, {mode: 0o700})
|
|
81
|
+
|
|
82
|
+
await assert.rejects(
|
|
83
|
+
() => prepareDaemonRuntime(privateLeaf),
|
|
84
|
+
/ancestor must be sticky or not writable by group or other users/
|
|
85
|
+
)
|
|
86
|
+
} finally {
|
|
87
|
+
await fs.chmod(unsafeAncestor, 0o700)
|
|
88
|
+
await fs.rm(unsafeAncestor, {force: true, recursive: true})
|
|
89
|
+
}
|
|
90
|
+
})
|
|
@@ -15,7 +15,7 @@ if (process.env.ROLLBRIDGE_TEST_STARTED_PATH) {
|
|
|
15
15
|
|
|
16
16
|
const server = http.createServer((request, response) => {
|
|
17
17
|
if (request.url === "/ping") {
|
|
18
|
-
if (healthFails) {
|
|
18
|
+
if (healthFails || (process.env.ROLLBRIDGE_TEST_HEALTH_GATE_PATH && !fs.existsSync(process.env.ROLLBRIDGE_TEST_HEALTH_GATE_PATH))) {
|
|
19
19
|
response.writeHead(500, {"Content-Type": "application/json"})
|
|
20
20
|
response.end(JSON.stringify({message: "bad release"}))
|
|
21
21
|
return
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict"
|
|
4
|
+
import {execFile} from "node:child_process"
|
|
4
5
|
import fs from "node:fs/promises"
|
|
6
|
+
import os from "node:os"
|
|
5
7
|
import path from "node:path"
|
|
6
8
|
import test from "node:test"
|
|
7
9
|
import {fileURLToPath} from "node:url"
|
|
10
|
+
import {promisify} from "node:util"
|
|
8
11
|
|
|
9
12
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
13
|
+
const execFileAsync = promisify(execFile)
|
|
10
14
|
|
|
11
15
|
test("package.json declares publish metadata", async () => {
|
|
12
16
|
const pkg = JSON.parse(await fs.readFile(path.join(repoRoot, "package.json"), "utf8"))
|
|
@@ -27,3 +31,38 @@ test("a LICENSE file matching the declared license exists", async () => {
|
|
|
27
31
|
assert.match(license, /MIT License/)
|
|
28
32
|
assert.match(license, /Copyright \(c\) \d{4} kaspernj/)
|
|
29
33
|
})
|
|
34
|
+
|
|
35
|
+
test("package manifest excludes unexpected operational and coverage files", async (t) => {
|
|
36
|
+
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-pack-"))
|
|
37
|
+
t.after(() => fs.rm(fixtureRoot, {force: true, recursive: true}))
|
|
38
|
+
|
|
39
|
+
await fs.cp(repoRoot, fixtureRoot, {
|
|
40
|
+
filter: (source) => {
|
|
41
|
+
const relative = path.relative(repoRoot, source)
|
|
42
|
+
return relative !== ".git" && relative !== "node_modules" && relative !== "tmp"
|
|
43
|
+
},
|
|
44
|
+
recursive: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const unexpectedTmpPath = path.join(fixtureRoot, "tmp", "worker-control", "unexpected-transcript.jsonl")
|
|
48
|
+
const unexpectedCoveragePath = path.join(fixtureRoot, "coverage", "unexpected.txt")
|
|
49
|
+
await Promise.all([
|
|
50
|
+
fs.mkdir(path.dirname(unexpectedTmpPath), {recursive: true}),
|
|
51
|
+
fs.mkdir(path.dirname(unexpectedCoveragePath), {recursive: true}),
|
|
52
|
+
])
|
|
53
|
+
await Promise.all([
|
|
54
|
+
fs.writeFile(unexpectedTmpPath, '{"operational":"state"}\n'),
|
|
55
|
+
fs.writeFile(unexpectedCoveragePath, "unexpected coverage output\n"),
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const {stdout} = await execFileAsync("npm", ["pack", "--dry-run", "--json"], {cwd: fixtureRoot})
|
|
59
|
+
/** @type {Array<{path: string}>} */
|
|
60
|
+
const packageFiles = JSON.parse(stdout)[0].files
|
|
61
|
+
const packagePaths = packageFiles.map((file) => file.path)
|
|
62
|
+
|
|
63
|
+
for (const requiredPath of ["LICENSE", "README.md", "bin/rollbridge", "package.json", "src/cli.js", "src/daemon-runtime.js"]) {
|
|
64
|
+
assert.ok(packagePaths.includes(requiredPath), `expected package to include ${requiredPath}`)
|
|
65
|
+
}
|
|
66
|
+
assert.ok(!packagePaths.some((packagePath) => packagePath === "tmp" || packagePath.startsWith("tmp/")))
|
|
67
|
+
assert.ok(!packagePaths.some((packagePath) => packagePath === "coverage" || packagePath.startsWith("coverage/")))
|
|
68
|
+
})
|