mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/impl-agent-sleep-wake
# Conflicts: # src/renderer/src/components/terminal-pane/agent-completion-poll-interval.ts # src/shared/process-table-snapshot-reader.ts
This commit is contained in:
@@ -433,13 +433,13 @@ jobs:
|
||||
# result's generation is authoritative either way.
|
||||
ISOLATE_RESULT="$(node dev/scripts/prepare-relay-production-capacity-canary.mjs \
|
||||
--director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \
|
||||
--cell-id "${TARGET_CELL_ID}" --mode isolate)"
|
||||
--cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode isolate)"
|
||||
echo "${ISOLATE_RESULT}"
|
||||
ISOLATE_GENERATION="$(jq -er '.generation' <<< "${ISOLATE_RESULT}")"
|
||||
echo "SELECTOR_GENERATION_AFTER_ISOLATE=${ISOLATE_GENERATION}" >> "${GITHUB_ENV}"
|
||||
node dev/scripts/prepare-relay-production-capacity-canary.mjs \
|
||||
--director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \
|
||||
--cell-id "${TARGET_CELL_ID}" --mode drain
|
||||
--cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode drain
|
||||
node dev/scripts/verify-relay-capacity-transition.mjs \
|
||||
--director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \
|
||||
--cell-id "${TARGET_CELL_ID}" --hard-cap "${EXPECTED_HARD_CAP}" \
|
||||
@@ -613,7 +613,7 @@ jobs:
|
||||
echo "MUTATION_STARTED=true" >> "${GITHUB_ENV}"
|
||||
ACTIVATE_RESULT="$(node dev/scripts/prepare-relay-production-capacity-canary.mjs \
|
||||
--director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \
|
||||
--cell-id "${TARGET_CELL_ID}" --mode activate)"
|
||||
--cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode activate)"
|
||||
echo "${ACTIVATE_RESULT}"
|
||||
SELECTOR_GENERATION_AFTER_ACTIVATE="$(jq -er '.generation' \
|
||||
<<< "${ACTIVATE_RESULT}")"
|
||||
@@ -652,7 +652,7 @@ jobs:
|
||||
test "${MUTATION_STARTED:-false}" = true || exit 0
|
||||
ISOLATE_RESULT="$(node dev/scripts/prepare-relay-production-capacity-canary.mjs \
|
||||
--director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \
|
||||
--cell-id "${TARGET_CELL_ID}" --mode isolate)"
|
||||
--cell-id "${TARGET_CELL_ID}" --approved-cells same-cap --mode isolate)"
|
||||
echo "${ISOLATE_RESULT}"
|
||||
# The isolate result carries the authoritative post-isolate generation;
|
||||
# fixed offsets are wrong whenever an earlier isolate was a no-op.
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
membershipWithStates,
|
||||
selectorCellState
|
||||
} from './relay-admission-selector.mjs'
|
||||
import { SAME_CAP_CELLS } from './relay-production-same-cap-wave.mjs'
|
||||
|
||||
const DIRECTOR_ORIGIN = 'https://relay.onorca.dev'
|
||||
export const PRODUCTION_CAPACITY_CELL_IDS = [
|
||||
@@ -31,6 +32,9 @@ function cellOrigin(cellId) {
|
||||
return `https://${cellId.slice('production-gce-'.length)}.relay.onorca.dev`
|
||||
}
|
||||
|
||||
// The same-cap roll covers the Asia cells the US-only capacity rollout never touches.
|
||||
const APPROVED_CELL_LISTS = { 'same-cap': SAME_CAP_CELLS }
|
||||
|
||||
export function parseProductionCapacityCellArguments(argv) {
|
||||
const values = {}
|
||||
for (let index = 0; index < argv.length; index += 2) {
|
||||
@@ -42,8 +46,15 @@ export function parseProductionCapacityCellArguments(argv) {
|
||||
if (!['isolate', 'drain', 'activate'].includes(values.mode)) {
|
||||
throw new Error('--mode must be isolate, drain, or activate')
|
||||
}
|
||||
const approvedList = values['approved-cells']
|
||||
if (approvedList !== undefined && !APPROVED_CELL_LISTS[approvedList]) {
|
||||
throw new Error('--approved-cells is not a known allowlist')
|
||||
}
|
||||
const approvedCellIds = approvedList === undefined
|
||||
? PRODUCTION_CAPACITY_CELL_IDS
|
||||
: APPROVED_CELL_LISTS[approvedList]
|
||||
const cellId = values['cell-id']
|
||||
if (!PRODUCTION_CAPACITY_CELL_IDS.includes(cellId)) {
|
||||
if (!approvedCellIds.includes(cellId)) {
|
||||
throw new Error('production capacity target is not approved')
|
||||
}
|
||||
const expectedCellOrigin = cellOrigin(cellId)
|
||||
|
||||
@@ -104,6 +104,47 @@ describe('production Relay capacity cell admission', () => {
|
||||
'--cell-id', 'production-gce-c7',
|
||||
'--mode', 'isolate'
|
||||
]), /origin is not exact/)
|
||||
assert.throws(() => parseProductionCapacityCellArguments([
|
||||
'--director-origin', 'https://relay.onorca.dev',
|
||||
'--cell-origin', 'https://c27.relay.onorca.dev',
|
||||
'--cell-id', 'production-gce-c27',
|
||||
'--mode', 'isolate'
|
||||
]), /not approved/)
|
||||
})
|
||||
|
||||
it('admits the same-cap Asia cells only under the same-cap allowlist', () => {
|
||||
for (const cellId of ['production-gce-c27', 'production-gce-c28', 'production-gce-c29']) {
|
||||
const hostname = cellId.slice('production-gce-'.length)
|
||||
assert.deepEqual(parseProductionCapacityCellArguments([
|
||||
'--director-origin', 'https://relay.onorca.dev',
|
||||
'--cell-origin', `https://${hostname}.relay.onorca.dev`,
|
||||
'--cell-id', cellId,
|
||||
'--approved-cells', 'same-cap',
|
||||
'--mode', 'isolate'
|
||||
]), {
|
||||
directorOrigin: 'https://relay.onorca.dev',
|
||||
cellOrigin: `https://${hostname}.relay.onorca.dev`,
|
||||
cellId,
|
||||
mode: 'isolate'
|
||||
})
|
||||
}
|
||||
for (const cellId of ['production-gce-c17', 'production-gce-c18', 'production-gce-c30']) {
|
||||
const hostname = cellId.slice('production-gce-'.length)
|
||||
assert.throws(() => parseProductionCapacityCellArguments([
|
||||
'--director-origin', 'https://relay.onorca.dev',
|
||||
'--cell-origin', `https://${hostname}.relay.onorca.dev`,
|
||||
'--cell-id', cellId,
|
||||
'--approved-cells', 'same-cap',
|
||||
'--mode', 'isolate'
|
||||
]), /not approved/)
|
||||
}
|
||||
assert.throws(() => parseProductionCapacityCellArguments([
|
||||
'--director-origin', 'https://relay.onorca.dev',
|
||||
'--cell-origin', 'https://c27.relay.onorca.dev',
|
||||
'--cell-id', 'production-gce-c27',
|
||||
'--approved-cells', 'every-cell',
|
||||
'--mode', 'isolate'
|
||||
]), /not a known allowlist/)
|
||||
})
|
||||
|
||||
it('isolates only the selected cell without depending on its runtime', async () => {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { describe, it } from 'node:test'
|
||||
import { parseProductionCapacityCellArguments } from './prepare-relay-production-capacity-canary.mjs'
|
||||
import { SAME_CAP_CELLS } from './relay-production-same-cap-wave.mjs'
|
||||
import { readRelayWorkflow } from './relay-repository.mjs'
|
||||
|
||||
const workflow = readRelayWorkflow('deploy-relay-production-same-cap-job.yml')
|
||||
const capacityWorkflow = readRelayWorkflow('deploy-relay-production-capacity-job.yml')
|
||||
|
||||
function hostname(cellId) {
|
||||
return cellId.slice('production-gce-'.length)
|
||||
}
|
||||
|
||||
// The job resolves cap and region from the cell id before any admin call; run that block alone.
|
||||
function resolveCellShape(cellId) {
|
||||
const start = workflow.indexOf(' TARGET_HOSTNAME="${TARGET_CELL_ID#production-gce-}"')
|
||||
assert.notEqual(start, -1, 'the same-cap cell shape block is missing')
|
||||
const end = workflow.indexOf('\n esac\n', start)
|
||||
assert.notEqual(end, -1, 'the same-cap cell shape block has no esac')
|
||||
const script = workflow.slice(start, end + '\n esac'.length).replace(/^ {10}/gm, '')
|
||||
return spawnSync('bash', [
|
||||
'-euo',
|
||||
'pipefail',
|
||||
'-c',
|
||||
`${script}\necho "\${EXPECTED_REGION} \${EXPECTED_HARD_CAP}"`
|
||||
], { env: { ...process.env, TARGET_CELL_ID: cellId }, encoding: 'utf8' })
|
||||
}
|
||||
|
||||
describe('same-cap roll scripts accept every same-cap cell', () => {
|
||||
it('parses every wave cell through the same-cap canary allowlist', () => {
|
||||
for (const cellId of SAME_CAP_CELLS) {
|
||||
for (const mode of ['isolate', 'drain', 'activate']) {
|
||||
assert.deepEqual(parseProductionCapacityCellArguments([
|
||||
'--director-origin', 'https://relay.onorca.dev',
|
||||
'--cell-origin', `https://${hostname(cellId)}.relay.onorca.dev`,
|
||||
'--cell-id', cellId,
|
||||
'--approved-cells', 'same-cap',
|
||||
'--mode', mode
|
||||
]), {
|
||||
directorOrigin: 'https://relay.onorca.dev',
|
||||
cellOrigin: `https://${hostname(cellId)}.relay.onorca.dev`,
|
||||
cellId,
|
||||
mode
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves a cap and region for every wave cell and refuses anything else', () => {
|
||||
for (const cellId of SAME_CAP_CELLS) {
|
||||
const resolved = resolveCellShape(cellId)
|
||||
assert.equal(resolved.status, 0, `${cellId}: ${resolved.stderr}`)
|
||||
assert.match(resolved.stdout.trim(), /^(us-central1 1000|asia-east2 3000)$/)
|
||||
}
|
||||
assert.equal(resolveCellShape('production-gce-c17').status, 1)
|
||||
assert.equal(resolveCellShape('production-gce-c30').status, 1)
|
||||
})
|
||||
|
||||
it('passes the same-cap allowlist on every canary invocation the job runs', () => {
|
||||
const invocations = workflow.split('prepare-relay-production-capacity-canary.mjs').slice(1)
|
||||
assert.equal(invocations.length, 4)
|
||||
for (const invocation of invocations) {
|
||||
const lines = invocation.split('\n')
|
||||
const end = lines.findIndex((line) => !line.endsWith('\\'))
|
||||
const call = lines.slice(0, end + 1).join(' ')
|
||||
assert.match(call, /--approved-cells same-cap/)
|
||||
assert.match(call, /--mode (isolate|drain|activate)/)
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves the US-only capacity job on the default allowlist', () => {
|
||||
assert.doesNotMatch(capacityWorkflow, /--approved-cells/)
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"load:relay:recovery-gate": "node dev/scripts/run-relay-recovery-wave-gate.mjs",
|
||||
"ops:relay": "pnpm --filter @orca-cloud/relay-ops dev",
|
||||
"pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-admission-workflow.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs",
|
||||
"test": "pnpm -r test && node --test dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-regional-rehome-workflow.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs",
|
||||
"test": "pnpm -r test && node --test dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-regional-rehome-workflow.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs",
|
||||
"typecheck": "pnpm -r typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -57,7 +57,7 @@ const { POLL_TIER_INTERVAL_MS } = await import(
|
||||
path.join(ROOT, 'src/renderer/src/components/terminal-pane/agent-completion-poll-cadence.ts')
|
||||
)
|
||||
const { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } = await import(
|
||||
path.join(ROOT, 'src/shared/process-table-snapshot-timing.ts')
|
||||
path.join(ROOT, 'src/shared/process-table-snapshot-reader.ts')
|
||||
)
|
||||
|
||||
// Pre-change: independent ±10% jitter per pane, re-rolled on every reschedule.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS } from '../../shared/git-fetch-auto-maintenance'
|
||||
import {
|
||||
measureRetargetDivergence,
|
||||
RETARGET_MAX_COMMIT_DIVERGENCE
|
||||
@@ -10,8 +11,13 @@ import {
|
||||
|
||||
const tempRoots: string[] = []
|
||||
|
||||
// Why the maintenance suppression: `git commit` detaches `git maintenance run --auto`, and its
|
||||
// commit-graph task arms once a fixture crosses 100 new commits — which the cap-sized histories
|
||||
// below always do. That detached process keeps writing `.git/objects/info/commit-graphs` after the
|
||||
// synchronous exec has returned, so it re-creates entries under a `.git/objects` the temp-root
|
||||
// teardown is midway through deleting, and the recursive remove dies with ENOTEMPTY.
|
||||
function git(cwd: string, args: string[]): string {
|
||||
return execFileSync('git', args, {
|
||||
return execFileSync('git', [...GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { build } from 'esbuild'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from '../../../../shared/process-table-snapshot-timing'
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from '../../../../shared/process-table-snapshot-reader'
|
||||
import { POLL_TIER_INTERVAL_MS } from './agent-completion-poll-cadence'
|
||||
import { nextCadenceInspectionDelayMs } from './agent-completion-poll-interval'
|
||||
|
||||
@@ -15,18 +14,6 @@ describe('nextCadenceInspectionDelayMs', () => {
|
||||
now
|
||||
})
|
||||
|
||||
it('keeps its production import graph browser-only', async () => {
|
||||
await expect(
|
||||
build({
|
||||
bundle: true,
|
||||
entryPoints: [`${import.meta.dirname}/agent-completion-poll-interval.ts`],
|
||||
logLevel: 'silent',
|
||||
platform: 'browser',
|
||||
write: false
|
||||
})
|
||||
).resolves.toMatchObject({ errors: [] })
|
||||
})
|
||||
|
||||
it('walks panes that scheduled at different moments onto one shared deadline', () => {
|
||||
// Why this matters: the inspection queue collapses shared-observation tasks enqueued in the
|
||||
// same tick onto one process-table capture, so a shared deadline is one `ps` for all panes.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from '../../../../shared/process-table-snapshot-timing'
|
||||
// Why not the sibling reader that re-exports this: it imports `node:child_process`, which the
|
||||
// renderer cannot load — reaching it blanks the window at module evaluation.
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from '../../../../shared/process-table-snapshot'
|
||||
|
||||
/**
|
||||
* Picks the delay until a pane's next cadence inspection.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* The renderer runs sandboxed with contextIsolation: `node:*` builtins do not resolve and even a
|
||||
* bare `process` read throws. A module that reaches one is not a degraded feature — the chunk
|
||||
* fails at evaluation, React never mounts, and the window stays blank with `workspaceSessionReady`
|
||||
* stuck false (#18742 did exactly this by importing one constant out of a `node:child_process`
|
||||
* module). Bundling hides it: the offending module can sit in a shared chunk far from the import
|
||||
* that pulled it in.
|
||||
*
|
||||
* So walk the import graph from every renderer entry — lazy routes included, since a `node:`
|
||||
* builtin behind one is just a blank route instead of a blank app — and refuse any builtin.
|
||||
*/
|
||||
const RENDERER_SRC = import.meta.dirname
|
||||
const REPO_SRC = path.resolve(RENDERER_SRC, '../..')
|
||||
const ENTRIES = ['main.tsx', 'popout.tsx', 'web/main.tsx']
|
||||
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']
|
||||
|
||||
/** `import`/`export ... from` and `import(...)` specifiers, minus type-only ones, which erase. */
|
||||
function collectValueImportSpecifiers(source: string): string[] {
|
||||
const specifiers: string[] = []
|
||||
const pattern =
|
||||
/(?:^|[\s;}])(?:import|export)(\s+type\s|\s*\{[^}]*\}|[^'"]*?)?\s*from\s*['"]([^'"]+)['"]|(?:^|[\s;}])import\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
const clause = match[1] ?? ''
|
||||
const specifier = match[2] ?? match[3] ?? match[4]
|
||||
if (!specifier || /^\s*type\s/.test(clause)) {
|
||||
continue
|
||||
}
|
||||
// A brace clause whose every binding is `type`-prefixed also erases entirely.
|
||||
const bindings = clause.trim().startsWith('{') ? clause.trim().slice(1, -1).split(',') : null
|
||||
if (bindings && bindings.some((b) => b.trim()) && bindings.every((b) => /^\s*type\s/.test(b))) {
|
||||
continue
|
||||
}
|
||||
specifiers.push(specifier)
|
||||
}
|
||||
return specifiers
|
||||
}
|
||||
|
||||
function resolveModule(specifier: string, fromFile: string): string | null {
|
||||
let base: string
|
||||
if (specifier.startsWith('@renderer/')) {
|
||||
base = path.join(RENDERER_SRC, specifier.slice('@renderer/'.length))
|
||||
} else if (specifier.startsWith('@/')) {
|
||||
base = path.join(RENDERER_SRC, specifier.slice(2))
|
||||
} else if (specifier.startsWith('.')) {
|
||||
base = path.resolve(path.dirname(fromFile), specifier)
|
||||
} else {
|
||||
// Bare package specifiers are npm dependencies, not first-party source.
|
||||
return null
|
||||
}
|
||||
for (const candidate of [
|
||||
...EXTENSIONS.map((ext) => `${base}${ext}`),
|
||||
...EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
|
||||
]) {
|
||||
if (existsSync(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function walkRendererImportGraph(): Map<string, string[]> {
|
||||
/** file -> the chain of first-party importers that reached it, entry first. */
|
||||
const pathToFile = new Map<string, string[]>()
|
||||
const queue: string[] = []
|
||||
for (const entry of ENTRIES) {
|
||||
const file = path.join(RENDERER_SRC, entry)
|
||||
pathToFile.set(file, [file])
|
||||
queue.push(file)
|
||||
}
|
||||
while (queue.length > 0) {
|
||||
const file = queue.shift() as string
|
||||
const chain = pathToFile.get(file) as string[]
|
||||
for (const specifier of collectValueImportSpecifiers(readFileSync(file, 'utf8'))) {
|
||||
const resolved = resolveModule(specifier, file)
|
||||
if (!resolved || pathToFile.has(resolved)) {
|
||||
continue
|
||||
}
|
||||
pathToFile.set(resolved, [...chain, resolved])
|
||||
queue.push(resolved)
|
||||
}
|
||||
}
|
||||
return pathToFile
|
||||
}
|
||||
|
||||
describe('renderer node-builtin boundary', () => {
|
||||
it('reaches no module that imports a node: builtin', () => {
|
||||
const graph = walkRendererImportGraph()
|
||||
const offenders: string[] = []
|
||||
for (const [file, chain] of graph) {
|
||||
const builtins = collectValueImportSpecifiers(readFileSync(file, 'utf8')).filter(
|
||||
(specifier) => specifier.startsWith('node:')
|
||||
)
|
||||
if (builtins.length === 0) {
|
||||
continue
|
||||
}
|
||||
const relativeChain = chain.map((step) => path.relative(REPO_SRC, step)).join('\n -> ')
|
||||
offenders.push(`${builtins.join(', ')} via\n ${relativeChain}`)
|
||||
}
|
||||
expect(offenders.join('\n\n')).toBe('')
|
||||
})
|
||||
|
||||
it('walks a real graph, so an empty offender list means something', () => {
|
||||
const graph = walkRendererImportGraph()
|
||||
expect(graph.size).toBeGreaterThan(3_000)
|
||||
expect(graph.has(path.join(REPO_SRC, 'shared/process-table-snapshot.ts'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -2,8 +2,8 @@ import { execFile as execFileCb } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { readLinuxProcessStartTimes } from './linux-process-start-times'
|
||||
import { withEvidenceBudget } from './process-table-evidence-budget'
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from './process-table-snapshot-timing'
|
||||
import {
|
||||
PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS,
|
||||
PS_ARGS,
|
||||
PS_MAX_BUFFER_BYTES,
|
||||
ProcessTableCaptureError,
|
||||
@@ -12,13 +12,12 @@ import {
|
||||
type ProcessTableRow
|
||||
} from './process-table-snapshot'
|
||||
|
||||
export { PS_ARGS, PS_MAX_BUFFER_BYTES }
|
||||
export { parseLinuxProcStatStartTime } from './linux-process-start-times'
|
||||
export {
|
||||
PROCESS_TABLE_EVIDENCE_BUDGET_MS,
|
||||
withEvidenceBudget
|
||||
} from './process-table-evidence-budget'
|
||||
export { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from './process-table-snapshot-timing'
|
||||
export { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS, PS_ARGS, PS_MAX_BUFFER_BYTES }
|
||||
|
||||
const execFile = promisify(execFileCb)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/** How long a completed shared process-table snapshot remains reusable, and the maximum cadence pull-forward. */
|
||||
export const PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS = 500
|
||||
@@ -13,9 +13,14 @@ export type ProcessTableRow = {
|
||||
command: string
|
||||
}
|
||||
|
||||
// Why guarded: this module is the renderer-safe half of the process-table pair, and the renderer
|
||||
// runs sandboxed with contextIsolation, where a bare `process` read throws at module evaluation
|
||||
// and takes the whole chunk — and the app — down with it. Only hosts ever run these argv.
|
||||
const HOST_IS_DARWIN = typeof process !== 'undefined' && process.platform === 'darwin'
|
||||
|
||||
/** Columns used by the evidence reader. Keep command last so its spaces survive parsing. */
|
||||
export const PS_ARGS = (
|
||||
process.platform === 'darwin'
|
||||
HOST_IS_DARWIN
|
||||
? ['-axo', 'pid=,ppid=,pgid=,tpgid=,stat=,tty=,lstart=,command=']
|
||||
: ['-axo', 'pid=,ppid=,pgid=,tpgid=,stat=,tty=,etimes=,command=']
|
||||
) as readonly string[]
|
||||
@@ -28,7 +33,7 @@ export const PS_ARGS = (
|
||||
* marker comes from `/proc/<pid>/stat` for the pane subtree only.
|
||||
*/
|
||||
export const CHEAP_PS_ARGS = (
|
||||
process.platform === 'darwin'
|
||||
HOST_IS_DARWIN
|
||||
? ['-axo', 'pid=,ppid=,pgid=,tpgid=,stat=,lstart=']
|
||||
: ['-axo', 'pid=,ppid=,pgid=,tpgid=,stat=']
|
||||
) as readonly string[]
|
||||
@@ -80,6 +85,14 @@ export function parseCheapProcessTableRows(stdout: string): CheapProcessTableRow
|
||||
// "unverifiable". Matches the sibling reader in pty-descendant-termination.ts.
|
||||
export const PS_MAX_BUFFER_BYTES = 32 * 1024 * 1024
|
||||
|
||||
/** How much older than its own await a TTL-cached capture may be, on top of the capture's own
|
||||
* duration. Reported ages carry both, so this alone is not the staleness bound.
|
||||
*
|
||||
* Why here and not beside the reader that applies it: the renderer's cadence scheduler pulls a
|
||||
* pane's next poll forward by at most this much, and the reader is a `node:child_process` module
|
||||
* the renderer must never reach. */
|
||||
export const PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS = 500
|
||||
|
||||
/**
|
||||
* Parse legacy or evidence-shaped `ps` output into rows. Tolerates CRLF so a
|
||||
* snapshot parsed on any host stays correct; `command` (last field) keeps its
|
||||
|
||||
Reference in New Issue
Block a user