@open-mercato/cli 0.6.8-develop.7099.1.7ba2771d06 → 0.7.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/.turbo/turbo-build.log +1 -7
- package/dist/agentic/guides/module-facts.json +598 -5508
- package/dist/agentic/guides/module-facts.v2.json +598 -5594
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +0 -1
- package/dist/agentic/guides/modules/api_keys/index.md +2 -4
- package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +5 -6
- package/dist/agentic/guides/modules/attachments/di-service-tokens.md +4 -5
- package/dist/agentic/guides/modules/attachments/exact-override-targets.md +5 -6
- package/dist/agentic/guides/modules/attachments/index.md +2 -2
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/di-registrations-rich.md +2 -3
- package/dist/agentic/guides/modules/auth/di-service-tokens.md +2 -3
- package/dist/agentic/guides/modules/auth/exact-override-targets.md +2 -3
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +0 -3
- package/dist/agentic/guides/modules/catalog/index.md +2 -2
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +8 -9
- package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +8 -9
- package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +0 -2
- package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/active-extension-bindings.md +3 -6
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +0 -1
- package/dist/agentic/guides/modules/customers/events.md +1 -2
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +0 -7
- package/dist/agentic/guides/modules/customers/index.md +4 -4
- package/dist/agentic/guides/modules/customers/umes-contributions.md +0 -1
- package/dist/agentic/guides/modules/customers/umes-hosts.md +0 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/exact-override-targets.md +0 -2
- package/dist/agentic/guides/modules/directory/index.md +3 -5
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/active-extension-bindings.md +0 -1
- package/dist/agentic/guides/modules/sales/contribution-resolutions.md +0 -1
- package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -5
- package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
- package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +0 -2
- package/dist/agentic/guides/modules/sales/index.md +3 -3
- package/dist/agentic/guides/modules/sales/setup.md +1 -1
- package/dist/agentic/guides/modules/sales/umes-contributions.md +0 -1
- package/dist/agentic/guides/modules/sales/umes-diagnostics.md +0 -1
- package/dist/agentic/guides/modules/sales/umes-hosts.md +0 -9
- package/dist/agentic/guides/modules/scheduler/cli-commands.md +0 -1
- package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +1 -3
- package/dist/agentic/guides/modules/scheduler/index.md +2 -2
- package/dist/agentic/guides/modules/scheduler/workers.md +1 -2
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/AGENTS.md.template +3 -3
- package/dist/agentic/shared/ai/harness/README.md +2 -2
- package/dist/agentic/shared/ai/harness/RELEASE.md +6 -8
- package/dist/agentic/shared/ai/harness/cases.json +0 -83
- package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
- package/dist/agentic/shared/ai/harness/validators.json +1 -1
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +12 -3
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +11 -37
- package/dist/lib/generators/ast/index.js +1 -4
- package/dist/lib/generators/ast/index.js.map +2 -2
- package/dist/lib/generators/ast/source-file.js +1 -8
- package/dist/lib/generators/ast/source-file.js.map +2 -2
- package/dist/lib/generators/ast/writers.js +1 -9
- package/dist/lib/generators/ast/writers.js.map +2 -2
- package/dist/lib/generators/module-registry.js +189 -424
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-manifest-shards.js +1 -4
- package/dist/lib/generators/route-manifest-shards.js.map +2 -2
- package/dist/lib/spawn.js +2 -3
- package/dist/lib/spawn.js.map +2 -2
- package/dist/lib/testing/integration.js +24 -162
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +0 -14
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -8
- package/src/lib/__tests__/spawn.test.ts +0 -23
- package/src/lib/generators/__tests__/metadata-extraction.test.ts +0 -44
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +8 -32
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -4
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +1 -1
- package/src/lib/generators/__tests__/module-subset.test.ts +72 -96
- package/src/lib/generators/__tests__/output-snapshots.test.ts +1 -1
- package/src/lib/generators/__tests__/route-manifest-shards.test.ts +0 -15
- package/src/lib/generators/__tests__/structural-contracts.test.ts +3 -7
- package/src/lib/generators/ast/index.ts +1 -2
- package/src/lib/generators/ast/source-file.ts +0 -13
- package/src/lib/generators/ast/writers.ts +1 -26
- package/src/lib/generators/module-registry.ts +227 -534
- package/src/lib/generators/route-manifest-shards.ts +1 -8
- package/src/lib/spawn.ts +4 -5
- package/src/lib/testing/__tests__/integration.test.ts +0 -435
- package/src/lib/testing/integration.ts +32 -254
- package/src/mercato.ts +0 -27
- package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +0 -11
- package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/acl-features.md +0 -13
- package/dist/agentic/guides/modules/channel_discord/ai-agents.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/backend-pages.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/cli-commands.md +0 -13
- package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +0 -14
- package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +0 -12
- package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +0 -26
- package/dist/agentic/guides/modules/channel_discord/index.md +0 -22
- package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/setup.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +0 -14
- package/dist/agentic/guides/modules/channel_discord/workers.md +0 -12
- package/dist/agentic/guides/modules/directory/di-registrations-rich.md +0 -12
- package/dist/agentic/guides/modules/directory/di-service-tokens.md +0 -12
- package/dist/agentic/guides/modules/documents/acl-features.md +0 -17
- package/dist/agentic/guides/modules/documents/backend-pages.md +0 -13
- package/dist/agentic/guides/modules/documents/contribution-resolutions.md +0 -27
- package/dist/agentic/guides/modules/documents/di-registrations-rich.md +0 -21
- package/dist/agentic/guides/modules/documents/domain-commands.md +0 -37
- package/dist/agentic/guides/modules/documents/encryption.md +0 -11
- package/dist/agentic/guides/modules/documents/entities.md +0 -21
- package/dist/agentic/guides/modules/documents/events.md +0 -25
- package/dist/agentic/guides/modules/documents/exact-override-targets.md +0 -39
- package/dist/agentic/guides/modules/documents/index.md +0 -25
- package/dist/agentic/guides/modules/documents/notifications.md +0 -13
- package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +0 -11
- package/dist/agentic/guides/modules/documents/setup.md +0 -11
- package/dist/agentic/guides/modules/documents/umes-contributions.md +0 -27
- package/dist/agentic/guides/modules/documents/umes-hosts.md +0 -36
- package/src/__tests__/resolve-queue-abandon-hook.test.ts +0 -39
- package/src/lib/generators/__tests__/__snapshots__/bootstrap-registrations.test.ts.snap +0 -14
- package/src/lib/generators/__tests__/__snapshots__/command-loaders.test.ts.snap +0 -23
- package/src/lib/generators/__tests__/ast-writers-escaping.test.ts +0 -64
- package/src/lib/generators/__tests__/bootstrap-registrations.test.ts +0 -176
- package/src/lib/generators/__tests__/command-loaders.test.ts +0 -239
- package/src/lib/generators/__tests__/generated-registry-imports.test.ts +0 -128
- package/src/lib/generators/__tests__/registry-variant-parity.test.ts +0 -258
|
@@ -39,16 +39,9 @@ export function getRouteManifestShardKey(kind: RouteManifestShardKind, pathname:
|
|
|
39
39
|
return firstSegment.toLowerCase()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function indentDeclaration(declaration: string): string {
|
|
43
|
-
return declaration
|
|
44
|
-
.split('\n')
|
|
45
|
-
.map((line) => (line.length > 0 ? ` ${line}` : line))
|
|
46
|
-
.join('\n')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
function renderArray(entries: RouteManifestShardEntry[]): string {
|
|
50
43
|
if (entries.length === 0) return '[]'
|
|
51
|
-
return `[\n${entries.map((entry) =>
|
|
44
|
+
return `[\n${entries.map((entry) => ` ${entry.declaration}`).join(',\n')}\n]`
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
function uniqueImports(entries: RouteManifestShardEntry[]): string[] {
|
package/src/lib/spawn.ts
CHANGED
|
@@ -28,22 +28,21 @@ function assertWindowsCmdSafeValue(value: string, label: string): string {
|
|
|
28
28
|
export function resolveSpawnCommand(
|
|
29
29
|
command: string,
|
|
30
30
|
commandArgs: string[] = [],
|
|
31
|
-
options: { platform?: NodeJS.Platform
|
|
31
|
+
options: { platform?: NodeJS.Platform } = {},
|
|
32
32
|
): {
|
|
33
33
|
command: string
|
|
34
34
|
args: string[]
|
|
35
|
-
spawnOptions: { shell?: boolean; windowsVerbatimArguments?: boolean
|
|
35
|
+
spawnOptions: { shell?: boolean; windowsVerbatimArguments?: boolean }
|
|
36
36
|
} {
|
|
37
37
|
const platform = options.platform ?? process.platform
|
|
38
38
|
const safeCommand = assertProcessSafeValue(command, 'Process command')
|
|
39
39
|
const safeArgs = commandArgs.map((arg, index) => assertProcessSafeValue(arg, `Process argument #${index + 1}`))
|
|
40
|
-
const detachedSpawnOptions = options.detached ? { detached: true } : {}
|
|
41
40
|
|
|
42
41
|
if (!isWindowsCmdScript(safeCommand, platform)) {
|
|
43
42
|
return {
|
|
44
43
|
command: safeCommand,
|
|
45
44
|
args: safeArgs,
|
|
46
|
-
spawnOptions: {
|
|
45
|
+
spawnOptions: {},
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -52,6 +51,6 @@ export function resolveSpawnCommand(
|
|
|
52
51
|
args: safeArgs.map((arg, index) =>
|
|
53
52
|
assertWindowsCmdSafeValue(arg, `Windows command argument #${index + 1}`),
|
|
54
53
|
),
|
|
55
|
-
spawnOptions: {
|
|
54
|
+
spawnOptions: {},
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -20,17 +20,8 @@ import {
|
|
|
20
20
|
shouldReuseBuildArtifacts,
|
|
21
21
|
acquireEphemeralRuntimeLock,
|
|
22
22
|
waitForApplicationReadiness,
|
|
23
|
-
createBoundedOutputBuffer,
|
|
24
|
-
formatCapturedOutput,
|
|
25
|
-
CAPTURED_OUTPUT_MAX_LENGTH,
|
|
26
|
-
killProcessTree,
|
|
27
|
-
terminateProcessTree,
|
|
28
|
-
registerEphemeralShutdownHandlers,
|
|
29
23
|
} from '../integration'
|
|
30
|
-
import type { CapturedOutputProcess, ShutdownProcessRef } from '../integration'
|
|
31
|
-
import { resolveSpawnCommand } from '../../spawn'
|
|
32
24
|
import { EventEmitter } from 'node:events'
|
|
33
|
-
import { spawn } from 'node:child_process'
|
|
34
25
|
import type { ChildProcess } from 'node:child_process'
|
|
35
26
|
|
|
36
27
|
const CACHE_TTL_ENV_VAR = 'OM_INTEGRATION_BUILD_CACHE_TTL_SECONDS'
|
|
@@ -933,430 +924,4 @@ describe('waitForApplicationReadiness', () => {
|
|
|
933
924
|
fetchSpy.mockRestore()
|
|
934
925
|
}
|
|
935
926
|
})
|
|
936
|
-
|
|
937
|
-
it('includes the captured stderr tail when the application process exits unexpectedly', async () => {
|
|
938
|
-
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async () => {
|
|
939
|
-
await sleep(20)
|
|
940
|
-
return { status: 503, ok: false, text: async () => '' } as unknown as Response
|
|
941
|
-
})
|
|
942
|
-
const fakeProcess = makeFakeProcess() as CapturedOutputProcess
|
|
943
|
-
fakeProcess.readCapturedOutput = () => 'Error: listen EADDRINUSE: address already in use :::5001'
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
const readiness = waitForApplicationReadiness('http://127.0.0.1:5001', fakeProcess, {
|
|
947
|
-
timeoutMs: 5_000,
|
|
948
|
-
intervalMs: 5,
|
|
949
|
-
})
|
|
950
|
-
setTimeout(() => fakeProcess.emit('exit', 1), 30)
|
|
951
|
-
await expect(readiness).rejects.toThrow(/exited before readiness check \(exit 1\)/)
|
|
952
|
-
await expect(readiness).rejects.toThrow(/EADDRINUSE/)
|
|
953
|
-
} finally {
|
|
954
|
-
fetchSpy.mockRestore()
|
|
955
|
-
}
|
|
956
|
-
})
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
describe('createBoundedOutputBuffer', () => {
|
|
960
|
-
it('keeps the most recent output and drops the oldest once the cap is exceeded', () => {
|
|
961
|
-
const buffer = createBoundedOutputBuffer()
|
|
962
|
-
const lineCount = 200
|
|
963
|
-
|
|
964
|
-
for (let index = 0; index < lineCount; index += 1) {
|
|
965
|
-
buffer.append(`line-${index}:${'x'.repeat(500)}\n`)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const result = buffer.read()
|
|
969
|
-
expect(result).toContain('…(truncated)…')
|
|
970
|
-
expect(result).toContain(`line-${lineCount - 1}:`)
|
|
971
|
-
expect(result).not.toContain('line-0:')
|
|
972
|
-
expect(result.length).toBeLessThanOrEqual(CAPTURED_OUTPUT_MAX_LENGTH + '…(truncated)…'.length)
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
it('returns the raw output untouched when it stays under the cap', () => {
|
|
976
|
-
const buffer = createBoundedOutputBuffer()
|
|
977
|
-
buffer.append('hello ')
|
|
978
|
-
buffer.append('world')
|
|
979
|
-
expect(buffer.read()).toBe('hello world')
|
|
980
|
-
})
|
|
981
|
-
})
|
|
982
|
-
|
|
983
|
-
describe('killProcessTree', () => {
|
|
984
|
-
it('kills the POSIX process group via a negated pid', () => {
|
|
985
|
-
const calls: Array<[number, NodeJS.Signals]> = []
|
|
986
|
-
killProcessTree(4242, 'SIGTERM', {
|
|
987
|
-
platform: 'linux',
|
|
988
|
-
killPosixProcessGroup: (pid, signal) => {
|
|
989
|
-
calls.push([pid, signal])
|
|
990
|
-
},
|
|
991
|
-
})
|
|
992
|
-
expect(calls).toEqual([[4242, 'SIGTERM']])
|
|
993
|
-
})
|
|
994
|
-
|
|
995
|
-
it('asks Windows for a graceful process-tree kill on SIGTERM', () => {
|
|
996
|
-
const calls: Array<[number, { forced: boolean }]> = []
|
|
997
|
-
killProcessTree(4242, 'SIGTERM', {
|
|
998
|
-
platform: 'win32',
|
|
999
|
-
killWindowsProcessTree: (pid, options) => {
|
|
1000
|
-
calls.push([pid, options])
|
|
1001
|
-
},
|
|
1002
|
-
})
|
|
1003
|
-
expect(calls).toEqual([[4242, { forced: false }]])
|
|
1004
|
-
})
|
|
1005
|
-
|
|
1006
|
-
it('escalates to a forced Windows process-tree kill on SIGKILL', () => {
|
|
1007
|
-
const calls: Array<[number, { forced: boolean }]> = []
|
|
1008
|
-
killProcessTree(4242, 'SIGKILL', {
|
|
1009
|
-
platform: 'win32',
|
|
1010
|
-
killWindowsProcessTree: (pid, options) => {
|
|
1011
|
-
calls.push([pid, options])
|
|
1012
|
-
},
|
|
1013
|
-
})
|
|
1014
|
-
expect(calls).toEqual([[4242, { forced: true }]])
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
it('negates the pid when falling back to the default POSIX killer', () => {
|
|
1018
|
-
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => true)
|
|
1019
|
-
try {
|
|
1020
|
-
killProcessTree(4242, 'SIGTERM', { platform: 'linux' })
|
|
1021
|
-
expect(killSpy).toHaveBeenCalledWith(-4242, 'SIGTERM')
|
|
1022
|
-
} finally {
|
|
1023
|
-
killSpy.mockRestore()
|
|
1024
|
-
}
|
|
1025
|
-
})
|
|
1026
|
-
})
|
|
1027
|
-
|
|
1028
|
-
describe('terminateProcessTree', () => {
|
|
1029
|
-
const makeFakeProcessWithPid = (pid: number): CapturedOutputProcess => {
|
|
1030
|
-
const fakeProcess = new EventEmitter() as unknown as CapturedOutputProcess
|
|
1031
|
-
fakeProcess.pid = pid
|
|
1032
|
-
return fakeProcess
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
it('does not throw when the process already exited on its own (ESRCH)', async () => {
|
|
1036
|
-
const fakeProcess = makeFakeProcessWithPid(4242)
|
|
1037
|
-
const esrchError = Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
|
|
1038
|
-
|
|
1039
|
-
await expect(
|
|
1040
|
-
terminateProcessTree(fakeProcess, {
|
|
1041
|
-
platform: 'linux',
|
|
1042
|
-
gracePeriodMs: 10,
|
|
1043
|
-
killPosixProcessGroup: () => {
|
|
1044
|
-
throw esrchError
|
|
1045
|
-
},
|
|
1046
|
-
}),
|
|
1047
|
-
).resolves.toBeUndefined()
|
|
1048
|
-
})
|
|
1049
|
-
|
|
1050
|
-
it('still kills the group of a process that already reported its exit, without waiting on it', async () => {
|
|
1051
|
-
const fakeProcess = makeFakeProcessWithPid(4242)
|
|
1052
|
-
fakeProcess.exitCode = 1
|
|
1053
|
-
const calls: NodeJS.Signals[] = []
|
|
1054
|
-
|
|
1055
|
-
const startedAt = Date.now()
|
|
1056
|
-
await terminateProcessTree(fakeProcess, {
|
|
1057
|
-
platform: 'linux',
|
|
1058
|
-
gracePeriodMs: 5_000,
|
|
1059
|
-
killPosixProcessGroup: (_pid, signal) => {
|
|
1060
|
-
calls.push(signal)
|
|
1061
|
-
},
|
|
1062
|
-
})
|
|
1063
|
-
|
|
1064
|
-
// A reaped leader means no `'exit'` event will ever settle the wait, not that its group is
|
|
1065
|
-
// empty — the descendants it spawned are exactly the orphan #5333 is about.
|
|
1066
|
-
expect(calls).toEqual(['SIGKILL'])
|
|
1067
|
-
expect(Date.now() - startedAt).toBeLessThan(1_000)
|
|
1068
|
-
})
|
|
1069
|
-
|
|
1070
|
-
it('resolves as soon as the process exits, without burning the rest of the grace period', async () => {
|
|
1071
|
-
const fakeProcess = makeFakeProcessWithPid(4242)
|
|
1072
|
-
const calls: NodeJS.Signals[] = []
|
|
1073
|
-
|
|
1074
|
-
const startedAt = Date.now()
|
|
1075
|
-
const termination = terminateProcessTree(fakeProcess, {
|
|
1076
|
-
platform: 'linux',
|
|
1077
|
-
gracePeriodMs: 5_000,
|
|
1078
|
-
killPosixProcessGroup: (_pid, signal) => {
|
|
1079
|
-
calls.push(signal)
|
|
1080
|
-
},
|
|
1081
|
-
})
|
|
1082
|
-
setTimeout(() => fakeProcess.emit('exit', 0), 10)
|
|
1083
|
-
await termination
|
|
1084
|
-
|
|
1085
|
-
expect(calls).toEqual(['SIGTERM'])
|
|
1086
|
-
expect(Date.now() - startedAt).toBeLessThan(1_000)
|
|
1087
|
-
})
|
|
1088
|
-
|
|
1089
|
-
it('escalates to SIGKILL when the tree outlives the grace period', async () => {
|
|
1090
|
-
// A live pid the escalation's `isProcessRunning` guard will confirm, without signalling anything
|
|
1091
|
-
// real — the injected killer records instead of killing.
|
|
1092
|
-
const fakeProcess = makeFakeProcessWithPid(process.pid)
|
|
1093
|
-
const calls: NodeJS.Signals[] = []
|
|
1094
|
-
|
|
1095
|
-
await terminateProcessTree(fakeProcess, {
|
|
1096
|
-
platform: 'linux',
|
|
1097
|
-
gracePeriodMs: 10,
|
|
1098
|
-
killPosixProcessGroup: (_pid, signal) => {
|
|
1099
|
-
calls.push(signal)
|
|
1100
|
-
},
|
|
1101
|
-
})
|
|
1102
|
-
|
|
1103
|
-
expect(calls).toEqual(['SIGTERM', 'SIGKILL'])
|
|
1104
|
-
})
|
|
1105
|
-
|
|
1106
|
-
it('propagates a non-ESRCH failure from the killer', async () => {
|
|
1107
|
-
const fakeProcess = makeFakeProcessWithPid(4242)
|
|
1108
|
-
const permissionError = Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
|
|
1109
|
-
|
|
1110
|
-
await expect(
|
|
1111
|
-
terminateProcessTree(fakeProcess, {
|
|
1112
|
-
platform: 'linux',
|
|
1113
|
-
killPosixProcessGroup: () => {
|
|
1114
|
-
throw permissionError
|
|
1115
|
-
},
|
|
1116
|
-
}),
|
|
1117
|
-
).rejects.toThrow(/EPERM/)
|
|
1118
|
-
})
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
describe('formatCapturedOutput', () => {
|
|
1122
|
-
it('keeps the stdout tail visible when stderr also captured output', () => {
|
|
1123
|
-
expect(formatCapturedOutput('a deprecation warning', 'the real failure')).toBe(
|
|
1124
|
-
'--- stderr ---\na deprecation warning\n--- stdout ---\nthe real failure',
|
|
1125
|
-
)
|
|
1126
|
-
})
|
|
1127
|
-
|
|
1128
|
-
it('returns the single populated stream unlabelled', () => {
|
|
1129
|
-
expect(formatCapturedOutput('only stderr', '')).toBe('only stderr')
|
|
1130
|
-
expect(formatCapturedOutput('', 'only stdout')).toBe('only stdout')
|
|
1131
|
-
expect(formatCapturedOutput('', '')).toBe('')
|
|
1132
|
-
})
|
|
1133
|
-
})
|
|
1134
|
-
|
|
1135
|
-
describe('registerEphemeralShutdownHandlers', () => {
|
|
1136
|
-
const makeProcessRef = () => {
|
|
1137
|
-
const listeners = new Map<string, Array<() => void>>()
|
|
1138
|
-
const raised: Array<[number, NodeJS.Signals]> = []
|
|
1139
|
-
const processRef: ShutdownProcessRef = {
|
|
1140
|
-
pid: 9999,
|
|
1141
|
-
once: ((event: string, handler: () => void) => {
|
|
1142
|
-
listeners.set(event, [...(listeners.get(event) ?? []), handler])
|
|
1143
|
-
return processRef
|
|
1144
|
-
}) as ShutdownProcessRef['once'],
|
|
1145
|
-
off: ((event: string, handler: () => void) => {
|
|
1146
|
-
listeners.set(event, (listeners.get(event) ?? []).filter((entry) => entry !== handler))
|
|
1147
|
-
return processRef
|
|
1148
|
-
}) as ShutdownProcessRef['off'],
|
|
1149
|
-
removeAllListeners: ((event: string) => {
|
|
1150
|
-
listeners.delete(event)
|
|
1151
|
-
return processRef
|
|
1152
|
-
}) as ShutdownProcessRef['removeAllListeners'],
|
|
1153
|
-
kill: ((pid: number, signal: NodeJS.Signals) => {
|
|
1154
|
-
raised.push([pid, signal])
|
|
1155
|
-
return true
|
|
1156
|
-
}) as ShutdownProcessRef['kill'],
|
|
1157
|
-
}
|
|
1158
|
-
const emit = (event: string) => {
|
|
1159
|
-
for (const handler of [...(listeners.get(event) ?? [])]) handler()
|
|
1160
|
-
}
|
|
1161
|
-
const listenerCount = (event: string) => (listeners.get(event) ?? []).length
|
|
1162
|
-
return { processRef, emit, raised, listenerCount }
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// The regression guard for the interrupt path: `detached: true` puts the app tree in its own
|
|
1166
|
-
// session, so unless the runner tears it down on SIGINT the tree survives Ctrl+C holding
|
|
1167
|
-
// `server-start.lock` — the exact orphan this harness exists to prevent.
|
|
1168
|
-
it.each<NodeJS.Signals>(['SIGINT', 'SIGTERM'])(
|
|
1169
|
-
'stops the environment and re-raises %s so the runner still exits on the signal',
|
|
1170
|
-
async (signal) => {
|
|
1171
|
-
const { processRef, emit, raised } = makeProcessRef()
|
|
1172
|
-
let stopCallCount = 0
|
|
1173
|
-
registerEphemeralShutdownHandlers({
|
|
1174
|
-
stop: async () => {
|
|
1175
|
-
stopCallCount += 1
|
|
1176
|
-
},
|
|
1177
|
-
killApplicationTree: () => {},
|
|
1178
|
-
processRef,
|
|
1179
|
-
})
|
|
1180
|
-
|
|
1181
|
-
emit(signal)
|
|
1182
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
1183
|
-
|
|
1184
|
-
expect(stopCallCount).toBe(1)
|
|
1185
|
-
expect(raised).toEqual([[9999, signal]])
|
|
1186
|
-
},
|
|
1187
|
-
)
|
|
1188
|
-
|
|
1189
|
-
it('re-raises the signal even when stopping the environment fails', async () => {
|
|
1190
|
-
const { processRef, emit, raised } = makeProcessRef()
|
|
1191
|
-
registerEphemeralShutdownHandlers({
|
|
1192
|
-
stop: async () => {
|
|
1193
|
-
throw new Error('teardown blew up')
|
|
1194
|
-
},
|
|
1195
|
-
killApplicationTree: () => {},
|
|
1196
|
-
processRef,
|
|
1197
|
-
})
|
|
1198
|
-
|
|
1199
|
-
emit('SIGINT')
|
|
1200
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
1201
|
-
|
|
1202
|
-
expect(raised).toEqual([[9999, 'SIGINT']])
|
|
1203
|
-
})
|
|
1204
|
-
|
|
1205
|
-
it('kills the application tree on the hard-exit sweep', () => {
|
|
1206
|
-
const { processRef, emit } = makeProcessRef()
|
|
1207
|
-
let killCallCount = 0
|
|
1208
|
-
registerEphemeralShutdownHandlers({
|
|
1209
|
-
stop: async () => {},
|
|
1210
|
-
killApplicationTree: () => {
|
|
1211
|
-
killCallCount += 1
|
|
1212
|
-
},
|
|
1213
|
-
processRef,
|
|
1214
|
-
})
|
|
1215
|
-
|
|
1216
|
-
emit('exit')
|
|
1217
|
-
expect(killCallCount).toBe(1)
|
|
1218
|
-
})
|
|
1219
|
-
|
|
1220
|
-
// The environment is restarted on retry, so handlers that are never removed stack one dead
|
|
1221
|
-
// closure per attempt until Node warns about a listener leak.
|
|
1222
|
-
it('removes every listener on dispose so repeated environment starts do not stack them', () => {
|
|
1223
|
-
const { processRef, listenerCount } = makeProcessRef()
|
|
1224
|
-
const first = registerEphemeralShutdownHandlers({
|
|
1225
|
-
stop: async () => {},
|
|
1226
|
-
killApplicationTree: () => {},
|
|
1227
|
-
processRef,
|
|
1228
|
-
})
|
|
1229
|
-
expect(listenerCount('exit')).toBe(1)
|
|
1230
|
-
expect(listenerCount('SIGINT')).toBe(1)
|
|
1231
|
-
|
|
1232
|
-
first.dispose()
|
|
1233
|
-
expect(listenerCount('exit')).toBe(0)
|
|
1234
|
-
expect(listenerCount('SIGINT')).toBe(0)
|
|
1235
|
-
expect(listenerCount('SIGTERM')).toBe(0)
|
|
1236
|
-
|
|
1237
|
-
registerEphemeralShutdownHandlers({
|
|
1238
|
-
stop: async () => {},
|
|
1239
|
-
killApplicationTree: () => {},
|
|
1240
|
-
processRef,
|
|
1241
|
-
}).dispose()
|
|
1242
|
-
expect(listenerCount('exit')).toBe(0)
|
|
1243
|
-
})
|
|
1244
|
-
})
|
|
1245
|
-
|
|
1246
|
-
// Every other process-lifecycle test here drives injected fakes, so nothing pins down the part
|
|
1247
|
-
// that can only fail against the real OS: whether `detached: true` plus a negated-pid signal
|
|
1248
|
-
// actually reaps the grandchildren the wrapper spawned. `sh` stands in for the `yarn start`
|
|
1249
|
-
// wrapper and `sleep` for the `mercato start`/Next tree it would leave behind.
|
|
1250
|
-
const describeOnPosix = process.platform === 'win32' ? describe.skip : describe
|
|
1251
|
-
|
|
1252
|
-
describeOnPosix('terminateProcessTree against a real detached process tree', () => {
|
|
1253
|
-
const isProcessAlive = (pid: number): boolean => {
|
|
1254
|
-
try {
|
|
1255
|
-
process.kill(pid, 0)
|
|
1256
|
-
return true
|
|
1257
|
-
} catch (error) {
|
|
1258
|
-
return (error as NodeJS.ErrnoException).code !== 'ESRCH'
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
const waitUntilGone = async (pid: number, timeoutMs = 5_000): Promise<boolean> => {
|
|
1263
|
-
const deadline = Date.now() + timeoutMs
|
|
1264
|
-
while (Date.now() < deadline) {
|
|
1265
|
-
if (!isProcessAlive(pid)) return true
|
|
1266
|
-
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
1267
|
-
}
|
|
1268
|
-
return !isProcessAlive(pid)
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
it('kills the grandchild the wrapper spawned, not just the wrapper itself', async () => {
|
|
1272
|
-
const resolvedSpawn = resolveSpawnCommand('/bin/sh', ['-c', 'sleep 30 & echo $!; wait'], {
|
|
1273
|
-
detached: true,
|
|
1274
|
-
})
|
|
1275
|
-
const wrapperProcess = spawn(resolvedSpawn.command, resolvedSpawn.args, {
|
|
1276
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1277
|
-
...resolvedSpawn.spawnOptions,
|
|
1278
|
-
}) as CapturedOutputProcess
|
|
1279
|
-
|
|
1280
|
-
let grandchildPid = 0
|
|
1281
|
-
try {
|
|
1282
|
-
grandchildPid = await new Promise<number>((resolve, reject) => {
|
|
1283
|
-
const timer = setTimeout(() => reject(new Error('timed out waiting for the grandchild pid')), 5_000)
|
|
1284
|
-
wrapperProcess.stdout?.once('data', (chunk: Buffer) => {
|
|
1285
|
-
clearTimeout(timer)
|
|
1286
|
-
resolve(Number.parseInt(chunk.toString().trim(), 10))
|
|
1287
|
-
})
|
|
1288
|
-
})
|
|
1289
|
-
|
|
1290
|
-
expect(Number.isInteger(grandchildPid)).toBe(true)
|
|
1291
|
-
// The detached spawn puts the wrapper in its own process group, so the group id is its pid.
|
|
1292
|
-
expect(grandchildPid).not.toBe(wrapperProcess.pid)
|
|
1293
|
-
expect(isProcessAlive(grandchildPid)).toBe(true)
|
|
1294
|
-
|
|
1295
|
-
await terminateProcessTree(wrapperProcess, { gracePeriodMs: 1_000 })
|
|
1296
|
-
|
|
1297
|
-
await expect(waitUntilGone(grandchildPid)).resolves.toBe(true)
|
|
1298
|
-
await expect(waitUntilGone(wrapperProcess.pid as number)).resolves.toBe(true)
|
|
1299
|
-
} finally {
|
|
1300
|
-
for (const pid of [grandchildPid, wrapperProcess.pid ?? 0]) {
|
|
1301
|
-
if (pid && isProcessAlive(pid)) {
|
|
1302
|
-
try {
|
|
1303
|
-
process.kill(pid, 'SIGKILL')
|
|
1304
|
-
} catch {}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
}, 20_000)
|
|
1309
|
-
|
|
1310
|
-
// The shape `waitForApplicationReadiness` reports: the `yarn` wrapper dies non-zero (port in use,
|
|
1311
|
-
// broken build, worker dead on boot) while the `mercato start`/Next tree it spawned lives on. The
|
|
1312
|
-
// leader is already reaped by the time teardown runs, so this is the case an exit short-circuit in
|
|
1313
|
-
// `terminateProcessTree` silently skips — leaving the orphan that still holds `server-start.lock`.
|
|
1314
|
-
it('kills a grandchild that outlived its already-exited wrapper', async () => {
|
|
1315
|
-
const resolvedSpawn = resolveSpawnCommand('/bin/sh', ['-c', 'sleep 30 & echo $!; exit 1'], {
|
|
1316
|
-
detached: true,
|
|
1317
|
-
})
|
|
1318
|
-
const wrapperProcess = spawn(resolvedSpawn.command, resolvedSpawn.args, {
|
|
1319
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1320
|
-
...resolvedSpawn.spawnOptions,
|
|
1321
|
-
}) as CapturedOutputProcess
|
|
1322
|
-
|
|
1323
|
-
let grandchildPid = 0
|
|
1324
|
-
try {
|
|
1325
|
-
// `'exit'` and the stdout `'data'` carrying the pid are independent events with no ordering
|
|
1326
|
-
// guarantee, so both are awaited separately. Waiting for `'close'` instead would deadlock:
|
|
1327
|
-
// the detached grandchild inherits this stdout pipe and holds it open for its whole lifetime.
|
|
1328
|
-
const withTimeout = <T,>(label: string, subscribe: (resolve: (value: T) => void) => void) =>
|
|
1329
|
-
new Promise<T>((resolve, reject) => {
|
|
1330
|
-
const timer = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), 5_000)
|
|
1331
|
-
subscribe((value) => {
|
|
1332
|
-
clearTimeout(timer)
|
|
1333
|
-
resolve(value)
|
|
1334
|
-
})
|
|
1335
|
-
})
|
|
1336
|
-
|
|
1337
|
-
const [pidText] = await Promise.all([
|
|
1338
|
-
withTimeout<string>('the grandchild pid', (resolve) =>
|
|
1339
|
-
wrapperProcess.stdout?.once('data', (chunk: Buffer) => resolve(chunk.toString())),
|
|
1340
|
-
),
|
|
1341
|
-
withTimeout<void>('the wrapper to exit', (resolve) => wrapperProcess.once('exit', () => resolve())),
|
|
1342
|
-
])
|
|
1343
|
-
grandchildPid = Number.parseInt(pidText.trim(), 10)
|
|
1344
|
-
|
|
1345
|
-
expect(Number.isInteger(grandchildPid)).toBe(true)
|
|
1346
|
-
expect(wrapperProcess.exitCode).toBe(1)
|
|
1347
|
-
expect(isProcessAlive(grandchildPid)).toBe(true)
|
|
1348
|
-
|
|
1349
|
-
await terminateProcessTree(wrapperProcess, { gracePeriodMs: 1_000 })
|
|
1350
|
-
|
|
1351
|
-
await expect(waitUntilGone(grandchildPid)).resolves.toBe(true)
|
|
1352
|
-
} finally {
|
|
1353
|
-
for (const pid of [grandchildPid, wrapperProcess.pid ?? 0]) {
|
|
1354
|
-
if (pid && isProcessAlive(pid)) {
|
|
1355
|
-
try {
|
|
1356
|
-
process.kill(pid, 'SIGKILL')
|
|
1357
|
-
} catch {}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
}, 20_000)
|
|
1362
927
|
})
|