mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.
What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.
"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.
Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").
Fix pattern
-----------
Rename for the domain role, not the structure:
-type FieldShape = 'list' | 'map' | 'whole'
-const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
+type FieldEncoding = 'list' | 'map' | 'whole'
+const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>
-function assertGitPushTargetShape(target: unknown): void
+function assertValidGitPushTarget(target: unknown): void
-function describeReadDirPathShape(p: string): ReadDirPathKind
+function classifyReadDirPath(p: string): ReadDirPathKind
Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).
No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.
Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.
* src/renderer/src/components/browser-pane/annotate/**:
in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
rect, ellipse, highlight. That is a genuine domain noun, and it pervades
every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
lucide exports the icon component as `Shapes`. The name is theirs, and the
matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
`shapedSidebar` is a persisted onboarding-checklist field and a telemetry
enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
property is what selects the ZodObject branch of the conditional type.
No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.
Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.
Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.
Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
"anti-slop/no-reflect-apply": "error",
|
||||
"anti-slop/no-reflect-get": "error",
|
||||
"anti-slop/no-runtime-typeof": "off",
|
||||
"anti-slop/no-shape-in-symbol-names": "off",
|
||||
"anti-slop/no-shape-in-symbol-names": "error",
|
||||
"anti-slop/no-unknown-parameters": "off",
|
||||
"anti-slop/no-unknown-returns": "off",
|
||||
"anti-slop/no-unknown-type-aliases": "error",
|
||||
@@ -55,6 +55,56 @@
|
||||
"rules": {
|
||||
"anti-slop/no-module-mocking": "off"
|
||||
}
|
||||
},
|
||||
// The exemptions below are file-scoped rather than inline `oxlint-disable` comments
|
||||
// because the root lint scan does not load this plugin, so an inline directive naming
|
||||
// an anti-slop rule always reads back as an unused directive there.
|
||||
//
|
||||
// In the screenshot annotator a "shape" is the drawn geometry -- pen, arrow, rect,
|
||||
// ellipse, highlight. A domain noun, and it pervades every symbol in the module.
|
||||
// mobile/src/test-support/rpc-recording is the golden recorder engine. recorder-digest.ts
|
||||
// hashes these files' RAW BYTES into every golden's `recorderSha256` header, so any edit
|
||||
// here -- a rename or even an added comment -- invalidates all 208 recordings. The exemption
|
||||
// is config-scoped for that reason: an inline directive would change the bytes it protects.
|
||||
{
|
||||
"files": ["**/test-support/rpc-recording/**"],
|
||||
"rules": {
|
||||
"anti-slop/no-shape-in-symbol-names": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/browser-pane/annotate/**"],
|
||||
"rules": {
|
||||
"anti-slop/no-shape-in-symbol-names": "off"
|
||||
}
|
||||
},
|
||||
// lucide exports the icon component as `Shapes`, and the matching REPO_LUCIDE_ICONS key
|
||||
// is the persisted icon name shared by the desktop picker and mobile.
|
||||
{
|
||||
"files": [
|
||||
"**/components/repo/repo-icon.tsx",
|
||||
"**/worktree-list/rows/repo-header-project-actions.tsx",
|
||||
"**/components/MobileRepoIcon.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"anti-slop/no-shape-in-symbol-names": "off"
|
||||
}
|
||||
},
|
||||
// `shapedSidebar` is a persisted onboarding-checklist field and a telemetry enum member;
|
||||
// renaming it would orphan saved state.
|
||||
{
|
||||
"files": ["**/src/shared/constants.ts", "**/src/shared/onboarding-state-types.ts"],
|
||||
"rules": {
|
||||
"anti-slop/no-shape-in-symbol-names": "off"
|
||||
}
|
||||
},
|
||||
// Matching zod's own literal `shape` property is what selects the ZodObject branch of
|
||||
// RpcSendInput's conditional type.
|
||||
{
|
||||
"files": ["**/rpc-contract/rpc-send-params.ts"],
|
||||
"rules": {
|
||||
"anti-slop/no-shape-in-symbol-names": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ for (let sample = 0; sample < 500; sample++) {
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const [shape, count] of [
|
||||
for (const [topology, count] of [
|
||||
['flat', 1000],
|
||||
['all-cycles', 1000],
|
||||
['mixed-cycles', 100],
|
||||
@@ -58,9 +58,9 @@ for (const [shape, count] of [
|
||||
['mixed-cycles', 1000]
|
||||
]) {
|
||||
const rows = Array.from({ length: count }, (_, index) =>
|
||||
row(index, shape === 'flat' ? undefined : index ^ 1)
|
||||
row(index, topology === 'flat' ? undefined : index ^ 1)
|
||||
)
|
||||
if (shape === 'mixed-cycles') {
|
||||
if (topology === 'mixed-cycles') {
|
||||
rows.unshift(row('root', undefined))
|
||||
}
|
||||
assert.deepEqual(after(rows), before(rows))
|
||||
@@ -83,7 +83,7 @@ for (const [shape, count] of [
|
||||
samples[arm].push({ wallMs, cpuMs: (used.user + used.system) / 30_000 })
|
||||
}
|
||||
}
|
||||
results.push({ shape, count, samples })
|
||||
results.push({ topology, count, samples })
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ baseline, node: process.version, parityGraphs: 500, results }, null, 2)
|
||||
|
||||
@@ -58,15 +58,19 @@ for (let trial = 0; trial < 5000; trial += 1) {
|
||||
|
||||
const results = []
|
||||
for (const count of [8, 32, 128, 512, 1024]) {
|
||||
for (const shape of ['flat', 'fanout', 'balanced', 'chain']) {
|
||||
for (const topology of ['flat', 'fanout', 'balanced', 'chain']) {
|
||||
const rows = Array.from({ length: count }, (_, index) => {
|
||||
const parent =
|
||||
shape === 'fanout' ? 0 : shape === 'balanced' ? Math.floor((index - 1) / 4) : index - 1
|
||||
topology === 'fanout'
|
||||
? 0
|
||||
: topology === 'balanced'
|
||||
? Math.floor((index - 1) / 4)
|
||||
: index - 1
|
||||
return {
|
||||
paneKey: `pane-${index}`,
|
||||
entry: {
|
||||
orchestration:
|
||||
index > 0 && shape !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined
|
||||
index > 0 && topology !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -92,7 +96,7 @@ for (const count of [8, 32, 128, 512, 1024]) {
|
||||
}
|
||||
results.push({
|
||||
count,
|
||||
shape,
|
||||
topology,
|
||||
iterations,
|
||||
meanMicrosecondsPerTree: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
|
||||
@@ -40,7 +40,7 @@ function measure(fn, input, repeats) {
|
||||
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
|
||||
}
|
||||
const results = []
|
||||
for (const [shape, input] of [
|
||||
for (const [inputCase, input] of [
|
||||
['ordinary Markdown', '# Hello\n\n<p>Use `Array<string>` and <b>bold</b>.</p>'],
|
||||
...[2048, 8192, 16384].map((length) => [
|
||||
`${length} underscore collision`,
|
||||
@@ -49,7 +49,7 @@ for (const [shape, input] of [
|
||||
]) {
|
||||
assert.equal(after(input), before(input))
|
||||
results.push({
|
||||
shape,
|
||||
inputCase,
|
||||
bytes: Buffer.byteLength(input),
|
||||
beforeMs: measure(before, input, 5),
|
||||
afterMs: measure(after, input, 15)
|
||||
|
||||
@@ -25,7 +25,7 @@ function median(fn, input, repeats) {
|
||||
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
|
||||
}
|
||||
const rows = []
|
||||
for (const [shape, input] of [
|
||||
for (const [label, input] of [
|
||||
['8KiB blank lines', '\n'.repeat(8192)],
|
||||
['16KiB blank lines', '\n'.repeat(16384)],
|
||||
['32KiB blank lines', '\n'.repeat(32768)],
|
||||
@@ -37,7 +37,7 @@ for (const [shape, input] of [
|
||||
const beforeMs = median(before, input, 3)
|
||||
const afterMs = median(redactString, input, 15)
|
||||
rows.push({
|
||||
shape,
|
||||
label,
|
||||
bytes: Buffer.byteLength(input),
|
||||
beforeMs,
|
||||
afterMs,
|
||||
|
||||
@@ -36,15 +36,15 @@ function measurePair(source) {
|
||||
|
||||
const results = []
|
||||
for (const size of [8192, 16384, 32768]) {
|
||||
for (const shape of ['no icon', 'rel without href', 'unterminated link starts']) {
|
||||
for (const variant of ['no icon', 'rel without href', 'unterminated link starts']) {
|
||||
const source =
|
||||
shape === 'unterminated link starts'
|
||||
variant === 'unterminated link starts'
|
||||
? '<link '.repeat(Math.floor(size / 6))
|
||||
: 'a'.repeat(size) + (shape === 'rel without href' ? ' rel:"icon"' : '')
|
||||
: 'a'.repeat(size) + (variant === 'rel without href' ? ' rel:"icon"' : '')
|
||||
assert.equal(extractIconHref(source), original(source))
|
||||
const { beforeMs, afterMs } = measurePair(source)
|
||||
results.push({
|
||||
shape,
|
||||
variant,
|
||||
bytes: Buffer.byteLength(source),
|
||||
beforeMs,
|
||||
afterMs,
|
||||
|
||||
@@ -45,9 +45,9 @@ const arms = {
|
||||
}
|
||||
const results = []
|
||||
for (const size of [20_000, 200_000, 600_000]) {
|
||||
for (const shape of ['lines', 'long-line']) {
|
||||
for (const lineLayout of ['lines', 'long-line']) {
|
||||
const phrase =
|
||||
shape === 'lines'
|
||||
lineLayout === 'lines'
|
||||
? 'Ordinary prose with a little `code`.\n'
|
||||
: 'Ordinary prose with a little `code`. '
|
||||
const content = phrase.repeat(Math.ceil(size / phrase.length)).slice(0, size)
|
||||
@@ -73,7 +73,7 @@ for (const size of [20_000, 200_000, 600_000]) {
|
||||
samples[arm].push({ ms, cpuMs: (cpu.user + cpu.system) / 20_000 })
|
||||
}
|
||||
}
|
||||
results.push({ size, shape, samples })
|
||||
results.push({ size, lineLayout, samples })
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
|
||||
@@ -213,13 +213,13 @@ function drain(iterate, data, meta) {
|
||||
}
|
||||
|
||||
function describeFrames(iterate, data, meta) {
|
||||
const shapes = []
|
||||
const descriptions = []
|
||||
for (const frame of iterate(data, meta)) {
|
||||
shapes.push(
|
||||
descriptions.push(
|
||||
`${Buffer.from(frame.bytes).toString('base64')}|${frame.seq ?? 'u'}|${frame.opcode ?? 'u'}`
|
||||
)
|
||||
}
|
||||
return shapes.join('\n')
|
||||
return descriptions.join('\n')
|
||||
}
|
||||
|
||||
const SURROGATE_PAIR = '\u{1f600}'
|
||||
|
||||
@@ -77,7 +77,7 @@ for (const input of [
|
||||
]) {
|
||||
assert.deepEqual(display(after, input), display(before, input))
|
||||
}
|
||||
for (const [shape, input] of [
|
||||
for (const [caseName, input] of [
|
||||
['tiny', 'ls -la'],
|
||||
['100KB', 'a b\n\t'.repeat(15000)],
|
||||
['1MB', 'a b\n\t'.repeat(150000)],
|
||||
@@ -110,5 +110,5 @@ for (const [shape, input] of [
|
||||
samples[label].push((cpu.user + cpu.system) / 20000)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ shape, samples }))
|
||||
console.log(JSON.stringify({ caseName, samples }))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function option(name) {
|
||||
|
||||
const cliVersion = option('cli')
|
||||
const autocrlf = option('autocrlf')
|
||||
const shape = option('shape')
|
||||
const placement = option('shape')
|
||||
// Why: PR branch names are untrusted workflow input. Keep them out of the
|
||||
// generated shell command and pass them to Node through the environment.
|
||||
const source = option('source') ?? process.env.SKILL_UPDATE_SOURCE
|
||||
@@ -30,7 +30,7 @@ const ref = option('ref') ?? process.env.SKILL_UPDATE_REF
|
||||
if (
|
||||
!cliVersion ||
|
||||
(autocrlf !== 'true' && autocrlf !== 'false') ||
|
||||
(shape !== 'symlink' && shape !== 'copy') ||
|
||||
(placement !== 'symlink' && placement !== 'copy') ||
|
||||
!source ||
|
||||
!ref ||
|
||||
!/^[^/\s]+\/[^/\s]+$/.test(source)
|
||||
@@ -103,7 +103,7 @@ async function seedPlacement(name, tag) {
|
||||
const providerRoot = path.join(home, '.claude', 'skills')
|
||||
const provider = path.join(providerRoot, name)
|
||||
await mkdir(providerRoot, { recursive: true })
|
||||
await (shape === 'copy'
|
||||
await (placement === 'copy'
|
||||
? cp(canonical, provider, { recursive: true })
|
||||
: symlink(canonical, provider, process.platform === 'win32' ? 'junction' : 'dir'))
|
||||
}
|
||||
@@ -211,20 +211,20 @@ try {
|
||||
await assertCurrentCanonical(targetName)
|
||||
const targetProviderAfter = await packageDigestAt(await realpath(targetProvider))
|
||||
const targetProviderStat = await lstat(targetProvider)
|
||||
if (shape === 'symlink' && !targetProviderStat.isSymbolicLink()) {
|
||||
if (placement === 'symlink' && !targetProviderStat.isSymbolicLink()) {
|
||||
throw new Error(`${targetName} provider alias was replaced with an independent copy`)
|
||||
}
|
||||
if (shape === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) {
|
||||
if (placement === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) {
|
||||
throw new Error(`${targetName} provider alias did not converge with the canonical update`)
|
||||
}
|
||||
if (
|
||||
shape === 'copy' &&
|
||||
placement === 'copy' &&
|
||||
targetProviderAfter !== targetProviderBefore &&
|
||||
targetProviderAfter !== currentSkill(targetName).packageDigest
|
||||
) {
|
||||
throw new Error('Independent provider copy changed to an unexpected package identity')
|
||||
}
|
||||
if (shape === 'copy') {
|
||||
if (placement === 'copy') {
|
||||
// Why: hosted 1.5.17 replaces copies with aliases while equivalent local runs
|
||||
// retain the copy. Both prove this input topology must remain ineligible.
|
||||
const outcome = targetProviderStat.isSymbolicLink()
|
||||
@@ -241,7 +241,7 @@ try {
|
||||
throw new Error('Targeted update changed the non-targeted control provider placement')
|
||||
}
|
||||
const controlProviderStat = await lstat(controlProvider)
|
||||
if (shape === 'symlink' && !controlProviderStat.isSymbolicLink()) {
|
||||
if (placement === 'symlink' && !controlProviderStat.isSymbolicLink()) {
|
||||
throw new Error('Targeted update changed the non-targeted control topology')
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -358,7 +358,7 @@ async function main() {
|
||||
sampleAggregation: BENCHMARK_SAMPLE_AGGREGATION,
|
||||
injectedLoginDelayMs: options.loginDelayMs,
|
||||
loginProbePreambleBytes: Buffer.byteLength(probeText.split('__ORCA_PATH__', 1)[0]),
|
||||
guestProcessShape: {
|
||||
guestProcessChain: {
|
||||
login: 'sh -> interactive login shell -> git',
|
||||
fast: 'env -> git'
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Palette,
|
||||
Rocket,
|
||||
Server,
|
||||
// `Shapes` is lucide's own export name; exempted in config/oxlint-anti-slop.json.
|
||||
Shapes,
|
||||
Sparkles,
|
||||
SquareTerminal,
|
||||
|
||||
@@ -243,7 +243,7 @@ function countLinearWork(run: () => void): WorkCounts {
|
||||
}
|
||||
}
|
||||
|
||||
function shape(sections: LinearIssueSection[]) {
|
||||
function summarizeSections(sections: LinearIssueSection[]) {
|
||||
return sections.map((section) => ({
|
||||
key: section.key,
|
||||
label: section.label,
|
||||
@@ -268,8 +268,8 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
|
||||
(linearGroupBy) => {
|
||||
const projection = mount({ linearGroupBy })
|
||||
expect(projection.linearBoardSections).toBe(projection.linearIssueSections)
|
||||
expect(shape(projection.linearBoardSections)).toEqual(
|
||||
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections)
|
||||
expect(summarizeSections(projection.linearBoardSections)).toEqual(
|
||||
summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -278,8 +278,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
|
||||
const projection = mount({ linearGroupBy: 'none' })
|
||||
const legacy = legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' })
|
||||
expect(projection.linearBoardSections).not.toBe(projection.linearIssueSections)
|
||||
expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections))
|
||||
expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections))
|
||||
expect(summarizeSections(projection.linearIssueSections)).toEqual(
|
||||
summarizeSections(legacy.listSections)
|
||||
)
|
||||
expect(summarizeSections(projection.linearBoardSections)).toEqual(
|
||||
summarizeSections(legacy.boardSections)
|
||||
)
|
||||
expect(projection.linearIssueSections.map((section) => section.key)).toEqual(['all'])
|
||||
expect(projection.linearBoardSections.length).toBeGreaterThan(1)
|
||||
expect(projection.linearListEntries.every((entry) => entry.type === 'issue')).toBe(true)
|
||||
@@ -293,8 +297,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
|
||||
linearGroupBy,
|
||||
linearOrderBy: order
|
||||
})
|
||||
expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections))
|
||||
expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections))
|
||||
expect(summarizeSections(projection.linearIssueSections)).toEqual(
|
||||
summarizeSections(legacy.listSections)
|
||||
)
|
||||
expect(summarizeSections(projection.linearBoardSections)).toEqual(
|
||||
summarizeSections(legacy.boardSections)
|
||||
)
|
||||
expect(projection.linearIssuesForView.map((issue) => issue.id)).toEqual(
|
||||
legacy.issuesForView.map((issue) => issue.id)
|
||||
)
|
||||
@@ -310,20 +318,24 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
|
||||
|
||||
const grouped = rerender({ linearGroupBy: 'status' })
|
||||
expect(grouped.linearBoardSections).toBe(grouped.linearIssueSections)
|
||||
expect(shape(grouped.linearBoardSections)).toEqual(
|
||||
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections)
|
||||
expect(summarizeSections(grouped.linearBoardSections)).toEqual(
|
||||
summarizeSections(
|
||||
legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections
|
||||
)
|
||||
)
|
||||
|
||||
const assignee = rerender({ linearGroupBy: 'assignee' })
|
||||
expect(assignee.linearBoardSections).toBe(assignee.linearIssueSections)
|
||||
expect(shape(assignee.linearBoardSections)).toEqual(
|
||||
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections)
|
||||
expect(summarizeSections(assignee.linearBoardSections)).toEqual(
|
||||
summarizeSections(
|
||||
legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections
|
||||
)
|
||||
)
|
||||
|
||||
const none = rerender({ linearGroupBy: 'none' })
|
||||
expect(none.linearBoardSections).not.toBe(none.linearIssueSections)
|
||||
expect(shape(none.linearBoardSections)).toEqual(
|
||||
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections)
|
||||
expect(summarizeSections(none.linearBoardSections)).toEqual(
|
||||
summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -333,8 +345,8 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
|
||||
const next = rerender({ linearGroupBy: 'priority', linearOrderBy: 'identifier' })
|
||||
expect(next.linearBoardSections).not.toBe(firstSections)
|
||||
expect(next.linearBoardSections).toBe(next.linearIssueSections)
|
||||
expect(shape(next.linearBoardSections)).toEqual(
|
||||
shape(
|
||||
expect(summarizeSections(next.linearBoardSections)).toEqual(
|
||||
summarizeSections(
|
||||
legacyProjection({
|
||||
...DEFAULT_INPUT,
|
||||
linearGroupBy: 'priority',
|
||||
@@ -369,17 +381,17 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
|
||||
const refreshed = rerender({ linearGroupBy: 'status', items: makeItems(50) })
|
||||
expect(refreshed.linearBoardSections).not.toBe(sections)
|
||||
expect(refreshed.linearBoardSections).toBe(refreshed.linearIssueSections)
|
||||
expect(shape(refreshed.linearBoardSections)).toEqual(shape(sections))
|
||||
expect(summarizeSections(refreshed.linearBoardSections)).toEqual(summarizeSections(sections))
|
||||
})
|
||||
|
||||
it('does not mutate the shared sections when the list entries are built', () => {
|
||||
const projection = mount({ linearGroupBy: 'status' })
|
||||
const before = shape(projection.linearIssueSections)
|
||||
const before = summarizeSections(projection.linearIssueSections)
|
||||
const entryIssueIds = projection.linearListEntries
|
||||
.filter((entry) => entry.type === 'issue')
|
||||
.map((entry) => (entry.type === 'issue' ? entry.issue.id : ''))
|
||||
expect(entryIssueIds).toHaveLength(50)
|
||||
expect(shape(projection.linearBoardSections)).toEqual(before)
|
||||
expect(summarizeSections(projection.linearBoardSections)).toEqual(before)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
UnsafeWindowsBatchArgumentsError,
|
||||
WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL
|
||||
} from '../../shared/windows-batch-spawn'
|
||||
import { isSkillsCliAgentKeyShaped, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys'
|
||||
import { isUsableSkillsCliAgentKey, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys'
|
||||
import {
|
||||
buildAgentFeatureSkillInstallArgs,
|
||||
buildAgentFeatureSkillUpdateArgs
|
||||
@@ -150,7 +150,7 @@ function resolveInstallAgentKeys(flags: Map<string, string | boolean>): string[]
|
||||
if (keys.length === 0) {
|
||||
throw new RuntimeClientError('invalid_argument', 'Missing required --agent')
|
||||
}
|
||||
const unusable = keys.find((key) => !isSkillsCliAgentKeyShaped(key))
|
||||
const unusable = keys.find((key) => !isUsableSkillsCliAgentKey(key))
|
||||
if (unusable !== undefined) {
|
||||
// Why: the skills CLI drops a value starting with `-`, which leaves it with
|
||||
// no target and installs into every agent it knows.
|
||||
|
||||
@@ -47,7 +47,7 @@ const CAN_DENY_READ = process.platform !== 'win32' && process.getuid?.() !== 0
|
||||
const INTERVAL_MS = 20_000
|
||||
const SESSIONS = ['aaaaaaaa', 'bbbbbbbb', 'cccccccc']
|
||||
|
||||
type RootShape = {
|
||||
type RootLayout = {
|
||||
name: string
|
||||
/** Where the unreachable root's transcripts live, and where its files go. */
|
||||
detachedRoot: (harness: SessionSearchIndexerHarness) => string
|
||||
@@ -59,7 +59,7 @@ type RootShape = {
|
||||
|
||||
const OPENCLAW_SESSION_DIR = join('agents', 'main', 'sessions')
|
||||
|
||||
const ROOT_SHAPES: RootShape[] = [
|
||||
const ROOT_LAYOUTS: RootLayout[] = [
|
||||
{
|
||||
name: 'roots discovery reports one per directory',
|
||||
detachedRoot: (harness) => harness.roots.claudeProjectsDir ?? '',
|
||||
@@ -81,7 +81,7 @@ const ROOT_SHAPES: RootShape[] = [
|
||||
}
|
||||
]
|
||||
|
||||
type UnreachableShape = {
|
||||
type UnreachableMode = {
|
||||
name: string
|
||||
needsDeniedRead: boolean
|
||||
/**
|
||||
@@ -97,7 +97,7 @@ type UnreachableShape = {
|
||||
attach: (root: string, transcriptDir: string, parked: string) => Promise<void>
|
||||
}
|
||||
|
||||
const UNREACHABLE_SHAPES: UnreachableShape[] = [
|
||||
const UNREACHABLE_MODES: UnreachableMode[] = [
|
||||
{
|
||||
name: 'the root itself is not there',
|
||||
needsDeniedRead: false,
|
||||
@@ -276,8 +276,8 @@ function indexedSessions(): string[] {
|
||||
.sort()
|
||||
}
|
||||
|
||||
for (const roots of ROOT_SHAPES) {
|
||||
for (const unreachable of UNREACHABLE_SHAPES) {
|
||||
for (const roots of ROOT_LAYOUTS) {
|
||||
for (const unreachable of UNREACHABLE_MODES) {
|
||||
describe.skipIf(unreachable.needsDeniedRead && !CAN_DENY_READ)(
|
||||
`${roots.name}, ${unreachable.name}`,
|
||||
() => {
|
||||
@@ -298,7 +298,7 @@ for (const roots of ROOT_SHAPES) {
|
||||
// pass, so the setup drives passes until the index has caught up.
|
||||
await driveUntilIndexed(SESSIONS.length * 2)
|
||||
const detachedIds = detachedPaths.map((_path, index) =>
|
||||
roots === ROOT_SHAPES[0]
|
||||
roots === ROOT_LAYOUTS[0]
|
||||
? fullSessionId(SESSIONS[index] ?? '')
|
||||
: (SESSIONS[index] ?? '')
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ const MAX_TERMS = 64
|
||||
// A query that quotes something from a transcript: camelCase, SCREAMING_SNAKE,
|
||||
// a dotted or snake_case name, a path, a filename, a PR number, a ticket, code
|
||||
// punctuation, or an error word.
|
||||
const LITERAL_SHAPE =
|
||||
const LITERAL_PATTERN =
|
||||
/[A-Za-z0-9_]*[a-z][A-Z][A-Za-z0-9_]*|\b[A-Z][A-Z0-9]{2,}(_[A-Z0-9]+)+\b|\b\w{2,}[._]\w{2,}\b|\b[\w.-]+\/[\w/.-]+\b|\b\w+\.(ts|tsx|js|jsx|py|rs|go|json|md|sh|yml|yaml|toml|c|cc|h|java|sql)\b|#\d{3,}|\b[A-Z]{2,6}-\d{2,}\b|[(){};=]|::|->|--\w|\b(Error|Exception|Traceback|error:|warning:)\b/
|
||||
const QUOTED = /"[^"]{3,}"|'[^']{3,}'/
|
||||
|
||||
@@ -36,7 +36,7 @@ export type SessionSearchQueryPlan = {
|
||||
}
|
||||
|
||||
export function isLiteralQuery(query: string): boolean {
|
||||
return QUOTED.test(query) || LITERAL_SHAPE.test(query)
|
||||
return QUOTED.test(query) || LITERAL_PATTERN.test(query)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { AiVaultScanOptions } from './session-scanner-types'
|
||||
// Agents whose session IS the directory holding the scanned file: everything
|
||||
// beside it belongs to the same session (rovo's session_context.json, grok's
|
||||
// chat_history.jsonl), so the directory is the only complete delete unit.
|
||||
const AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS = new Set<AiVaultDeletableAgent>([
|
||||
const AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS = new Set<AiVaultDeletableAgent>([
|
||||
'rovo',
|
||||
'grok',
|
||||
'cline'
|
||||
@@ -109,7 +109,7 @@ function sessionDeleteRemovals(args: {
|
||||
}): readonly AiVaultSessionDeleteRemoval[] | null {
|
||||
const { agent, resolvedPath, matchedRoot, roots } = args
|
||||
|
||||
if (AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS.has(agent)) {
|
||||
if (AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS.has(agent)) {
|
||||
const sessionDir = dirname(resolvedPath)
|
||||
if (sessionDir === matchedRoot || !isPathInsideOrEqual(matchedRoot, sessionDir)) {
|
||||
return null
|
||||
|
||||
@@ -33,7 +33,7 @@ type FixtureResult = {
|
||||
afterCookies: JarCookie[]
|
||||
}
|
||||
|
||||
type SourceShape = {
|
||||
type SourceCookieRow = {
|
||||
name: string
|
||||
samesite: number | null
|
||||
is_secure: number
|
||||
@@ -147,18 +147,26 @@ run().catch((error) => {
|
||||
`
|
||||
}
|
||||
|
||||
function readSourceShape(sourceDbPath: string): SourceShape[] {
|
||||
function readSourceCookieRows(sourceDbPath: string): SourceCookieRow[] {
|
||||
const db = new DatabaseSync(sourceDbPath, { readOnly: true })
|
||||
try {
|
||||
return db
|
||||
.prepare('SELECT name, samesite, is_secure FROM cookies ORDER BY rowid')
|
||||
.all() as SourceShape[]
|
||||
.all()
|
||||
.map((row) => ({
|
||||
name: String(row.name),
|
||||
samesite: row.samesite === null ? null : Number(row.samesite),
|
||||
is_secure: Number(row.is_secure)
|
||||
}))
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: SourceShape[] }> {
|
||||
async function runFixture(): Promise<{
|
||||
fixture: FixtureResult
|
||||
sourceCookieRows: SourceCookieRow[]
|
||||
}> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-samesite-enum-'))
|
||||
fixtureRoots.push(root)
|
||||
const bundlePath = join(root, 'cookie-import-samesite.cjs')
|
||||
@@ -176,7 +184,7 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour
|
||||
})
|
||||
)
|
||||
createChromiumCookieTestDatabase(sourceDbPath, rows).close()
|
||||
const sourceShape = readSourceShape(sourceDbPath)
|
||||
const sourceCookieRows = readSourceCookieRows(sourceDbPath)
|
||||
writeFileSync(
|
||||
bundleEntryPath,
|
||||
`export { importCookiesFromBrowser } from ${JSON.stringify(join(process.cwd(), 'src/main/browser/browser-cookie-import.ts'))}`
|
||||
@@ -212,22 +220,23 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour
|
||||
const fixtureResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
|
||||
expect(run.error).toBeUndefined()
|
||||
expect(run.status, `${fixtureResult}\n${run.stdout}\n${run.stderr}`).toBe(0)
|
||||
return { fixture: JSON.parse(fixtureResult) as FixtureResult, sourceShape }
|
||||
const fixture: FixtureResult = JSON.parse(fixtureResult)
|
||||
return { fixture, sourceCookieRows }
|
||||
}
|
||||
|
||||
describe('Chromium SameSite storage enum import', () => {
|
||||
let fixture: FixtureResult
|
||||
let sourceShape: SourceShape[]
|
||||
let sourceCookieRows: SourceCookieRow[]
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ fixture, sourceShape } = await runFixture())
|
||||
;({ fixture, sourceCookieRows } = await runFixture())
|
||||
}, 120_000)
|
||||
|
||||
it('runs the real Chromium import against the complete synthetic matrix', () => {
|
||||
expect(fixture.step).toBe('import finished')
|
||||
expect(fixture.beforeCookieCount).toBe(0)
|
||||
expect(fixture.importResult.ok).toBe(true)
|
||||
expect(sourceShape).toEqual(
|
||||
expect(sourceCookieRows).toEqual(
|
||||
[REJECTION_CONTROL, ...VALID_COMBINATIONS, NULL_CASE].map(
|
||||
({ name, rawSameSite, secure }) => ({
|
||||
name,
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('ClaudeRuntimeAuthService', () => {
|
||||
it('rejects wrong-shaped refreshed credentials during read-back', async () => {
|
||||
const runtimeCredentialsPath = join(testState.fakeHomeDir, '.claude', '.credentials.json')
|
||||
const originalCredentials = createClaudeCredentialsJson('user@example.com', 'original')
|
||||
const wrongShapedRefresh = `${JSON.stringify({
|
||||
const malformedRefresh = `${JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
email: 'user@example.com',
|
||||
expiresAt: Date.now() + 120_000
|
||||
@@ -91,7 +91,7 @@ describe('ClaudeRuntimeAuthService', () => {
|
||||
settings.activeClaudeManagedAccountId = 'account-1'
|
||||
await service.syncForCurrentSelection()
|
||||
|
||||
writeFileSync(runtimeCredentialsPath, wrongShapedRefresh, 'utf-8')
|
||||
writeFileSync(runtimeCredentialsPath, malformedRefresh, 'utf-8')
|
||||
await service.syncForCurrentSelection()
|
||||
|
||||
expect(readManagedCredentialsForTest('account-1', managedAuthPath)).toBe(originalCredentials)
|
||||
|
||||
@@ -806,7 +806,7 @@ describe('codex item bodies', () => {
|
||||
// Both the row label and the run header read top-level input keys only, so a
|
||||
// shape whose detail sits inside `action` renders as the input's raw JSON.
|
||||
const url = 'https://example.com/docs/page'
|
||||
const shapes: [string, unknown, string, string][] = [
|
||||
const cases: [string, unknown, string, string][] = [
|
||||
['started', null, '', ''],
|
||||
[
|
||||
'search',
|
||||
@@ -823,7 +823,7 @@ describe('codex item bodies', () => {
|
||||
],
|
||||
['other', { type: 'other' }, 'other', '']
|
||||
]
|
||||
for (const [name, action, label, brief] of shapes) {
|
||||
for (const [name, action, label, brief] of cases) {
|
||||
// Codex leaves the item's own `query` empty on most completed searches.
|
||||
const query = name === 'search' || name === 'findInPage' ? 'a sample query' : ''
|
||||
const input = toolCallInput({ type: 'webSearch', id: 'w', query, action })
|
||||
|
||||
@@ -168,13 +168,13 @@ export class CursorHookService {
|
||||
}
|
||||
const cleaned = removeManagedCommands(definitions, isManagedCommand)
|
||||
// Also strip entries with the command at the top level (Cursor schema).
|
||||
const strippedCursorShape = cleaned.filter(
|
||||
const strippedTopLevelCommands = cleaned.filter(
|
||||
(definition) => !isManagedCommand(definition.command)
|
||||
)
|
||||
if (strippedCursorShape.length === 0) {
|
||||
if (strippedTopLevelCommands.length === 0) {
|
||||
delete nextHooks[eventName]
|
||||
} else {
|
||||
nextHooks[eventName] = strippedCursorShape
|
||||
nextHooks[eventName] = strippedTopLevelCommands
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GitPushTarget } from '../../shared/worktree/types'
|
||||
import { assertGitPushTargetShape } from '../../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../../shared/git-push-target-validation'
|
||||
import { gitExecFileAsync } from './runner'
|
||||
import type { GitExecOptions as GitCommandExecOptions } from './command-runner/git-exec-options'
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function validateGitPushTarget(
|
||||
target: unknown,
|
||||
options: GitExecOptions = {}
|
||||
): Promise<GitPushTarget> {
|
||||
assertGitPushTargetShape(target)
|
||||
assertValidGitPushTarget(target)
|
||||
await gitExecFileAsync(['check-ref-format', '--branch', target.branchName], {
|
||||
cwd: repoPath,
|
||||
...options
|
||||
|
||||
@@ -592,9 +592,9 @@ describe('GitHub GraphQL rate-limit guard', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ stackShape: 'omits stack', stackField: {} },
|
||||
{ stackShape: 'sets stack to null', stackField: { stack: null } }
|
||||
])('keeps legacy merge when an ordinary GitHub response $stackShape', async (scenario) => {
|
||||
{ stackVariant: 'omits stack', stackField: {} },
|
||||
{ stackVariant: 'sets stack to null', stackField: { stack: null } }
|
||||
])('keeps legacy merge when an ordinary GitHub response $stackVariant', async (scenario) => {
|
||||
ghExecFileAsyncMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
|
||||
@@ -164,7 +164,7 @@ function primeGitExecForDefaultBranch({
|
||||
})
|
||||
}
|
||||
|
||||
type RestPRShape = {
|
||||
type RestPROverrides = {
|
||||
number?: number
|
||||
state?: string
|
||||
merged_at?: string | null
|
||||
@@ -178,7 +178,7 @@ function restPR({
|
||||
merged_at = null,
|
||||
head_ref = 'master',
|
||||
head_sha = 'stale-master-oid'
|
||||
}: RestPRShape = {}): Record<string, unknown> {
|
||||
}: RestPROverrides = {}): Record<string, unknown> {
|
||||
return {
|
||||
number,
|
||||
title: 'Historical PR',
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
classifyProjectError,
|
||||
driftError,
|
||||
rateLimitedError,
|
||||
type GhGraphqlErrorShape
|
||||
type GhGraphqlError
|
||||
} from './project-error-classification'
|
||||
|
||||
export {
|
||||
@@ -172,7 +172,7 @@ export async function runGraphql<T>(
|
||||
...(exec?.host ? { host: exec.host } : {})
|
||||
})
|
||||
try {
|
||||
const parsed = JSON.parse(stdout) as { data?: T; errors?: GhGraphqlErrorShape[] }
|
||||
const parsed: { data?: T; errors?: GhGraphqlError[] } = JSON.parse(stdout)
|
||||
if (parsed.errors && parsed.errors.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import type { GitHubProjectViewError } from '../../../shared/github/project-result-types'
|
||||
import { githubProjectHost } from '../../../shared/github/project-identity'
|
||||
|
||||
export type GhGraphqlErrorShape = {
|
||||
export type GhGraphqlError = {
|
||||
type?: string
|
||||
message?: string
|
||||
path?: (string | number)[]
|
||||
extensions?: { code?: string }
|
||||
}
|
||||
|
||||
export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlErrorShape[] {
|
||||
export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlError[] {
|
||||
// `gh api graphql` prints the response JSON to stdout even on GraphQL
|
||||
// errors, and the stderr carries a summary. Try stdout first; if parsing
|
||||
// fails, fall back to stderr.
|
||||
@@ -21,7 +21,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(src) as { errors?: GhGraphqlErrorShape[] }
|
||||
const parsed: { errors?: GhGraphqlError[] } = JSON.parse(src)
|
||||
if (parsed.errors && parsed.errors.length > 0) {
|
||||
return parsed.errors
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE
|
||||
return []
|
||||
}
|
||||
|
||||
export function errorsIndicateParentField(errors: GhGraphqlErrorShape[], stderr: string): boolean {
|
||||
export function errorsIndicateParentField(errors: GhGraphqlError[], stderr: string): boolean {
|
||||
const lower = stderr.toLowerCase()
|
||||
// Preview-header shape: gh returns a 4xx with "preview" in the message.
|
||||
if (lower.includes('preview') && lower.includes('parent')) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
classifyProjectError,
|
||||
driftError,
|
||||
rateLimitedError,
|
||||
type GhGraphqlErrorShape
|
||||
type GhGraphqlError
|
||||
} from './project-error-classification'
|
||||
import { ownerQueryRoot } from './project-view-config'
|
||||
import type { RawItem } from './project-view-item-normalization'
|
||||
@@ -47,7 +47,7 @@ export async function fetchItemsPageWithRaw(args: {
|
||||
| {
|
||||
ok: false
|
||||
error: GitHubProjectViewError
|
||||
rawErrors: GhGraphqlErrorShape[]
|
||||
rawErrors: GhGraphqlError[]
|
||||
stderr: string
|
||||
}
|
||||
> {
|
||||
@@ -117,7 +117,7 @@ export async function fetchItemsPageWithRaw(args: {
|
||||
stdout = extracted.stdout
|
||||
execFailed = true
|
||||
}
|
||||
let parsed: { data?: Record<string, unknown>; errors?: GhGraphqlErrorShape[] } = {}
|
||||
let parsed: { data?: Record<string, unknown>; errors?: GhGraphqlError[] } = {}
|
||||
try {
|
||||
parsed = JSON.parse(stdout)
|
||||
} catch {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '../../../providers/ssh-git-dispatch'
|
||||
import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache'
|
||||
import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options'
|
||||
import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation'
|
||||
import {
|
||||
materializeWorktreePushTargetRemote,
|
||||
materializeWorktreePushTargetRemoteSsh
|
||||
@@ -35,7 +35,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
|
||||
const publish = args.publish === true
|
||||
if (args.connectionId) {
|
||||
if (args.pushTarget) {
|
||||
assertGitPushTargetShape(args.pushTarget)
|
||||
assertValidGitPushTarget(args.pushTarget)
|
||||
}
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
@@ -99,7 +99,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
|
||||
): Promise<void> => {
|
||||
if (args.connectionId) {
|
||||
if (args.pushTarget) {
|
||||
assertGitPushTargetShape(args.pushTarget)
|
||||
assertValidGitPushTarget(args.pushTarget)
|
||||
}
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
@@ -159,7 +159,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
|
||||
): Promise<void> => {
|
||||
if (args.connectionId) {
|
||||
if (args.pushTarget) {
|
||||
assertGitPushTargetShape(args.pushTarget)
|
||||
assertValidGitPushTarget(args.pushTarget)
|
||||
}
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '../../../providers/ssh-git-dispatch'
|
||||
import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache'
|
||||
import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options'
|
||||
import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation'
|
||||
import { validateGitForkSyncExpectedUpstream } from '../../../../shared/git-fork-sync'
|
||||
import {
|
||||
materializeWorktreePushTargetRemote,
|
||||
@@ -34,7 +34,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext)
|
||||
): Promise<GitUpstreamStatus> => {
|
||||
if (args.connectionId) {
|
||||
if (args.pushTarget) {
|
||||
assertGitPushTargetShape(args.pushTarget)
|
||||
assertValidGitPushTarget(args.pushTarget)
|
||||
}
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
@@ -65,7 +65,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext)
|
||||
): Promise<void> => {
|
||||
if (args.connectionId) {
|
||||
if (args.pushTarget) {
|
||||
assertGitPushTargetShape(args.pushTarget)
|
||||
assertValidGitPushTarget(args.pushTarget)
|
||||
}
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildReadDirErrorBreadcrumb, describeReadDirPathShape } from './readdir-error-diagnostics'
|
||||
import { buildReadDirErrorBreadcrumb, classifyReadDirPath } from './readdir-error-diagnostics'
|
||||
|
||||
describe('describeReadDirPathShape', () => {
|
||||
describe('classifyReadDirPath', () => {
|
||||
it('classifies a WSL UNC path without leaking it', () => {
|
||||
const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', undefined)
|
||||
expect(shape).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true })
|
||||
const classification = classifyReadDirPath(
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\u\\repo',
|
||||
undefined
|
||||
)
|
||||
expect(classification).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true })
|
||||
})
|
||||
|
||||
it('classifies the legacy \\\\wsl$ root as WSL', () => {
|
||||
expect(describeReadDirPathShape('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true)
|
||||
expect(classifyReadDirPath('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true)
|
||||
})
|
||||
|
||||
it('classifies a plain network UNC share as UNC but not WSL', () => {
|
||||
const shape = describeReadDirPathShape('\\\\fileserver\\share\\dir', undefined)
|
||||
expect(shape).toMatchObject({ isUNC: true, isWsl: false })
|
||||
expect(shape.driveLetter).toBeUndefined()
|
||||
const classification = classifyReadDirPath('\\\\fileserver\\share\\dir', undefined)
|
||||
expect(classification).toMatchObject({ isUNC: true, isWsl: false })
|
||||
expect(classification.driveLetter).toBeUndefined()
|
||||
})
|
||||
|
||||
it('extracts an uppercased drive letter for mapped drives', () => {
|
||||
expect(describeReadDirPathShape('z:\\projects\\repo', undefined)).toEqual({
|
||||
expect(classifyReadDirPath('z:\\projects\\repo', undefined)).toEqual({
|
||||
hasConnectionId: false,
|
||||
isUNC: false,
|
||||
isWsl: false,
|
||||
@@ -27,18 +30,18 @@ describe('describeReadDirPathShape', () => {
|
||||
})
|
||||
|
||||
it('flags the SSH connection without recording it', () => {
|
||||
const shape = describeReadDirPathShape('/remote/repo', 'ssh-1')
|
||||
expect(shape).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false })
|
||||
const classification = classifyReadDirPath('/remote/repo', 'ssh-1')
|
||||
expect(classification).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false })
|
||||
})
|
||||
|
||||
it('never includes the raw path in the shape', () => {
|
||||
const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9')
|
||||
expect(JSON.stringify(shape)).not.toContain('secret')
|
||||
it('never includes the raw path in the classification', () => {
|
||||
const classification = classifyReadDirPath('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9')
|
||||
expect(JSON.stringify(classification)).not.toContain('secret')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildReadDirErrorBreadcrumb', () => {
|
||||
it('captures throw site, error code/name, and path shape', () => {
|
||||
it('captures throw site, error code/name, and path classification', () => {
|
||||
const breadcrumb = buildReadDirErrorBreadcrumb({
|
||||
dirPath: '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo',
|
||||
connectionId: undefined,
|
||||
|
||||
@@ -11,7 +11,7 @@ export type ReadDirThrowSite = 'ssh-provider' | 'authorize' | 'readdir'
|
||||
* even though breadcrumbs are path-redacted downstream, never collecting the
|
||||
* raw path is the safer default.
|
||||
*/
|
||||
export function describeReadDirPathShape(
|
||||
export function classifyReadDirPath(
|
||||
dirPath: string,
|
||||
connectionId: string | undefined
|
||||
): CrashReportBreadcrumbData {
|
||||
@@ -52,6 +52,6 @@ export function buildReadDirErrorBreadcrumb(args: {
|
||||
throwSite: args.throwSite,
|
||||
errorName: args.error instanceof Error ? args.error.name : typeof args.error,
|
||||
...(errorCode(args.error) ? { errorCode: errorCode(args.error)! } : {}),
|
||||
...describeReadDirPathShape(args.dirPath, args.connectionId)
|
||||
...classifyReadDirPath(args.dirPath, args.connectionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
|
||||
import { getHostedReviewForBranch } from '../source-control/hosted-review'
|
||||
import type { ForgeProviderId } from '../source-control/forge-provider'
|
||||
import { validateGitPushTarget } from '../git/push-target-validation'
|
||||
import { assertGitPushTargetShape } from '../../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../../shared/git-push-target-validation'
|
||||
import { gitExecFileAsync } from '../git/runner'
|
||||
import type {
|
||||
OrcaRuntimeService,
|
||||
@@ -1277,7 +1277,7 @@ export async function prepareWorktreePushTargetSsh(
|
||||
store?: WorktreePushTargetStore,
|
||||
repoId?: string
|
||||
): Promise<GitPushTarget> {
|
||||
assertGitPushTargetShape(target)
|
||||
assertValidGitPushTarget(target)
|
||||
const execGit: GitRemoteExec = (args, cwd) => provider.exec(args, cwd)
|
||||
const { remoteCreated: _ignoredRemoteCreated, ...sanitizedTarget } = target
|
||||
await provider.exec(['check-ref-format', '--branch', target.branchName], repoPath)
|
||||
|
||||
@@ -63,14 +63,12 @@ function serializedLifecycleBatchFits(
|
||||
fence: Number.MAX_SAFE_INTEGER,
|
||||
ts: Number.MAX_SAFE_INTEGER,
|
||||
settlementId,
|
||||
mutations: mutations.map(lifecycleMutationRowShape)
|
||||
mutations: mutations.map(toLifecycleMutationRow)
|
||||
}
|
||||
return Buffer.byteLength(JSON.stringify(row), 'utf8') + 1 <= MAX_JOURNAL_LIFECYCLE_BATCH_BYTES
|
||||
}
|
||||
|
||||
function lifecycleMutationRowShape(
|
||||
mutation: JournalLifecycleMutationInput
|
||||
): JournalLifecycleMutation {
|
||||
function toLifecycleMutationRow(mutation: JournalLifecycleMutationInput): JournalLifecycleMutation {
|
||||
const itemId = agentJournalItemKey(mutation.identity)
|
||||
return mutation.kind === 'item'
|
||||
? {
|
||||
|
||||
+5
-5
@@ -75,14 +75,14 @@ function item(index: number, sequence: number): AgentJournalRenderItem {
|
||||
}
|
||||
}
|
||||
|
||||
/** Every sequence-run shape of `length` items, as run-length compositions. */
|
||||
function* runShapes(length: number): Generator<number[]> {
|
||||
/** Every run-length composition of `length` items. */
|
||||
function* runLengthCompositions(length: number): Generator<number[]> {
|
||||
if (length === 0) {
|
||||
yield []
|
||||
return
|
||||
}
|
||||
for (let first = 1; first <= length; first += 1) {
|
||||
for (const rest of runShapes(length - first)) {
|
||||
for (const rest of runLengthCompositions(length - first)) {
|
||||
yield [first, ...rest]
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ function buildItems(runs: number[], repeatSequence: boolean): AgentJournalRender
|
||||
it('matches eager grouping at every newest-window limit for every run shape', () => {
|
||||
let cases = 0
|
||||
for (let length = 0; length <= 7; length += 1) {
|
||||
for (const runs of runShapes(length)) {
|
||||
for (const runs of runLengthCompositions(length)) {
|
||||
for (const repeatSequence of [false, true]) {
|
||||
const items = buildItems(runs, repeatSequence)
|
||||
// Every boundary, including 0, each exact group edge, and past the end.
|
||||
@@ -127,7 +127,7 @@ it('matches eager byte bounding at every budget boundary in both directions', ()
|
||||
let truncatedCases = 0
|
||||
let partialCases = 0
|
||||
for (let length = 1; length <= 6; length += 1) {
|
||||
for (const runs of runShapes(length)) {
|
||||
for (const runs of runLengthCompositions(length)) {
|
||||
for (const repeatSequence of [false, true]) {
|
||||
const items = buildItems(runs, repeatSequence)
|
||||
const perItem = historyEntryBytes(items[0]!, submissionBytes)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CODEX_APP_SERVER_NOTIFICATION_METHODS } from '../../codex/codex-app-ser
|
||||
import { CLAUDE_STREAM_JSON_FRAME_KINDS } from './claude-stream-json-frame-schema'
|
||||
import {
|
||||
classifyProviderFrame,
|
||||
isDeltaShapedProviderFrameKind,
|
||||
isDeltaProviderFrameKind,
|
||||
PROVIDER_FRAME_CLASSIFICATIONS
|
||||
} from './provider-frame-disposition'
|
||||
import { unhandledProviderFrameJournalItem } from './unhandled-provider-frame'
|
||||
@@ -25,7 +25,7 @@ describe('provider frame classification catalog', () => {
|
||||
const deltaKinds = [
|
||||
...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.codex),
|
||||
...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.claude)
|
||||
].filter(isDeltaShapedProviderFrameKind)
|
||||
].filter(isDeltaProviderFrameKind)
|
||||
|
||||
expect(deltaKinds.length).toBeGreaterThan(0)
|
||||
for (const kind of deltaKinds) {
|
||||
|
||||
@@ -226,7 +226,7 @@ function itemKind(kind: string): string | null {
|
||||
return kind.startsWith('item:') ? kind.slice('item:'.length) : null
|
||||
}
|
||||
|
||||
export function isDeltaShapedProviderFrameKind(kind: string): boolean {
|
||||
export function isDeltaProviderFrameKind(kind: string): boolean {
|
||||
return notificationKind(kind).toLowerCase().endsWith('delta')
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export function classifyProviderFrame(
|
||||
if (hasProviderError(payload)) {
|
||||
return 'error-surface'
|
||||
}
|
||||
if (isDeltaShapedProviderFrameKind(kind)) {
|
||||
if (isDeltaProviderFrameKind(kind)) {
|
||||
return 'stream-into-item'
|
||||
}
|
||||
if (provider === 'claude' && kind === 'message:result') {
|
||||
|
||||
@@ -25,7 +25,7 @@ const SECRETS = {
|
||||
pem: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ\n-----END PRIVATE KEY-----'
|
||||
}
|
||||
|
||||
const SHAPES: { label: string; raw: string; tag: string }[] = [
|
||||
const PROVIDER_KEY_CASES: { label: string; raw: string; tag: string }[] = [
|
||||
{ label: 'anthropic', raw: SECRETS.anthropic, tag: 'anthropic-key' },
|
||||
{ label: 'openai', raw: SECRETS.openai, tag: 'openai-key' },
|
||||
{ label: 'github', raw: SECRETS.github, tag: 'github-token' },
|
||||
@@ -37,7 +37,7 @@ const SHAPES: { label: string; raw: string; tag: string }[] = [
|
||||
]
|
||||
|
||||
describe('redactor — provider-key fingerprints', () => {
|
||||
for (const { label, raw, tag } of SHAPES) {
|
||||
for (const { label, raw, tag } of PROVIDER_KEY_CASES) {
|
||||
describe(`${label}`, () => {
|
||||
it('redacts when the secret appears as an attribute value', () => {
|
||||
// Bare "<secret>" without a labeled-kv keyword nearby — exercises the
|
||||
|
||||
@@ -138,7 +138,7 @@ function writeLegacyFile(dataFile: string): void {
|
||||
|
||||
/** Inverse of everything this change does, applied to a compact file: what the old serializer
|
||||
* would have written for the same state. */
|
||||
function reexpandToLegacyShape(state: PersistedState): PersistedState {
|
||||
function reexpandToLegacySerialization(state: PersistedState): PersistedState {
|
||||
const expanded = structuredClone(state)
|
||||
for (const map of [expanded.worktreeMeta, expanded.worktreeMetaByIdentity]) {
|
||||
for (const [key, meta] of Object.entries(map ?? {})) {
|
||||
@@ -207,7 +207,7 @@ describe('persisted-state redundancy', () => {
|
||||
// Apples to apples: re-expand the file we just wrote back into the old shape and compare, so
|
||||
// the number is the redundancy alone and not the settings defaults a synthetic fixture lacks.
|
||||
expect(Buffer.byteLength(rewritten)).toBeLessThan(
|
||||
Buffer.byteLength(JSON.stringify(reexpandToLegacyShape(onDisk))) * 0.6
|
||||
Buffer.byteLength(JSON.stringify(reexpandToLegacySerialization(onDisk))) * 0.6
|
||||
)
|
||||
|
||||
// load(save(state)) deep-equals the pre-save state for every field touched.
|
||||
|
||||
@@ -8,7 +8,7 @@ const userDataDir = mkdtempSync(join(tmpdir(), 'orca-pi-overlay-path-userdata-')
|
||||
|
||||
import { PiTitlebarExtensionService } from './titlebar-extension-service'
|
||||
|
||||
const PATH_SHAPED_PTY_ID = [
|
||||
const PATH_LIKE_PTY_ID = [
|
||||
'50c010a2-bc8e-4eb1-8847-5812133ad6df',
|
||||
'Users',
|
||||
'dev',
|
||||
@@ -45,7 +45,7 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
|
||||
try {
|
||||
const env = svc.buildPtyEnv(PATH_SHAPED_PTY_ID, piHome, 'pi')
|
||||
const env = svc.buildPtyEnv(PATH_LIKE_PTY_ID, piHome, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
@@ -61,12 +61,12 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => {
|
||||
})
|
||||
|
||||
it('clears legacy raw path-shaped daemon overlays during teardown', () => {
|
||||
const legacyOverlayDir = legacyOverlayPath('pi', PATH_SHAPED_PTY_ID)
|
||||
const legacyOverlayDir = legacyOverlayPath('pi', PATH_LIKE_PTY_ID)
|
||||
mkdirSync(legacyOverlayDir, { recursive: true })
|
||||
writeFileSync(join(legacyOverlayDir, 'stale.txt'), 'stale overlay')
|
||||
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
svc.clearPty(PATH_SHAPED_PTY_ID)
|
||||
svc.clearPty(PATH_LIKE_PTY_ID)
|
||||
|
||||
expect(existsSync(legacyOverlayDir)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -132,10 +132,10 @@ function* legacyIterateTerminalOutputFrameChunks(
|
||||
}
|
||||
}
|
||||
|
||||
type FrameShape = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' }
|
||||
type FrameSummary = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' }
|
||||
|
||||
function describeFrames(frames: Iterable<TerminalOutputFrameChunk>): FrameShape[] {
|
||||
const out: FrameShape[] = []
|
||||
function describeFrames(frames: Iterable<TerminalOutputFrameChunk>): FrameSummary[] {
|
||||
const out: FrameSummary[] = []
|
||||
for (const frame of frames) {
|
||||
out.push({
|
||||
base64: Buffer.from(frame.bytes).toString('base64'),
|
||||
@@ -170,10 +170,10 @@ const SURROGATE_EDGES = [
|
||||
'\udfff\udc00'
|
||||
]
|
||||
|
||||
// Meta shapes exercised against every fixture: no meta, seq-preserved (rawLength ===
|
||||
// Meta variants exercised against every fixture: no meta, seq-preserved (rawLength ===
|
||||
// data.length), the delayed-final-seq path (rawLength !== data.length -> OutputSpan),
|
||||
// transformed, and cwd-only.
|
||||
function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] {
|
||||
function metaVariantsFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] {
|
||||
return [
|
||||
{ label: 'no-meta', meta: undefined },
|
||||
{ label: 'seq-only', meta: { seq: 5_000_000 } },
|
||||
@@ -187,8 +187,8 @@ function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta
|
||||
}
|
||||
|
||||
function sweepAll(data: string, label: string): void {
|
||||
for (const shape of metaShapesFor(data)) {
|
||||
expectEquivalent(data, shape.meta, `${label} [${shape.label}]`)
|
||||
for (const variant of metaVariantsFor(data)) {
|
||||
expectEquivalent(data, variant.meta, `${label} [${variant.label}]`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@ export function computeTerminalTailWaitState(
|
||||
partialLine: string,
|
||||
preview: string
|
||||
): TerminalTailWaitState {
|
||||
const tailShape = inspectTerminalWaitTail(lines, partialLine)
|
||||
if (!tailShape.fromTail) {
|
||||
const tailInspection = inspectTerminalWaitTail(lines, partialLine)
|
||||
if (!tailInspection.fromTail) {
|
||||
return {
|
||||
waitText: preview,
|
||||
signal: findActionableTerminalWaitBlockedSignal(preview.toLowerCase()),
|
||||
fromTail: false
|
||||
}
|
||||
}
|
||||
if (!tailShape.mayContainBlockedSignal) {
|
||||
if (!tailInspection.mayContainBlockedSignal) {
|
||||
// Why: reads waitText only when a signal exists; avoid retaining a rebuilt 256 KiB string in the common case.
|
||||
return { waitText: '', signal: null, fromTail: true }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export type SkillBundleArtifacts = {
|
||||
}
|
||||
|
||||
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
const snapshotShape = {
|
||||
const snapshotFields = {
|
||||
releaseRevision: z.number().int().positive(),
|
||||
packageDigest: sha256Schema,
|
||||
gitTreeSha: z.string().regex(/^[a-f0-9]{40}$/),
|
||||
@@ -38,7 +38,7 @@ const snapshotShape = {
|
||||
)
|
||||
.min(1)
|
||||
}
|
||||
const knownSnapshotSchema = z.object(snapshotShape).strict()
|
||||
const knownSnapshotSchema = z.object(snapshotFields).strict()
|
||||
const manifestSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(2),
|
||||
@@ -47,7 +47,7 @@ const manifestSchema = z
|
||||
.object({
|
||||
name: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
||||
sourcePath: z.string().min(1),
|
||||
...snapshotShape
|
||||
...snapshotFields
|
||||
})
|
||||
.strict()
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('a host key store written by a newer version', () => {
|
||||
const storeFile = join(dir, 'ssh-host-keys.json')
|
||||
const future = JSON.stringify({
|
||||
version: 99,
|
||||
hostKeys: [{ shape: 'we do not understand' }]
|
||||
hostKeys: [{ unrecognized: 'we do not understand' }]
|
||||
})
|
||||
await writeFile(storeFile, future, 'utf-8')
|
||||
|
||||
|
||||
@@ -167,9 +167,9 @@ describe('what the host publishes about a pane, read by the sweep', () => {
|
||||
it('records that a backgrounded and a suspended shell are indistinguishable at tpgid/pgid', () => {
|
||||
// The premise of the whole file. If this ever fails, the fixtures drifted and every verdict
|
||||
// below is testing something other than the defect. Pids differ between captures, so the
|
||||
// comparison is of the shell row's shape: who its parent is, whether it leads its own process
|
||||
// group, whether that group owns the terminal, and its state flags.
|
||||
const shellShape = (capture: { rootPid: number; table: readonly string[] }): string => {
|
||||
// comparison is of the shell row's signature: who its parent is, whether it leads its own
|
||||
// process group, whether that group owns the terminal, and its state flags.
|
||||
const shellRowSignature = (capture: { rootPid: number; table: readonly string[] }): string => {
|
||||
const row = parseStrictProcessTableRows(capture.table.join('\n')).find(
|
||||
(candidate) => candidate.pid === capture.rootPid
|
||||
)!
|
||||
@@ -181,19 +181,21 @@ describe('what the host publishes about a pane, read by the sweep', () => {
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
expect(shellShape(CAPTURES.idle)).toBe('ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+')
|
||||
expect(shellShape(CAPTURES.background)).toBe(shellShape(CAPTURES.idle))
|
||||
expect(shellShape(CAPTURES.ctrlz)).toBe(shellShape(CAPTURES.idle))
|
||||
expect(shellShape(CAPTURES.foreground)).not.toBe(shellShape(CAPTURES.idle))
|
||||
expect(shellRowSignature(CAPTURES.idle)).toBe(
|
||||
'ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+'
|
||||
)
|
||||
expect(shellRowSignature(CAPTURES.background)).toBe(shellRowSignature(CAPTURES.idle))
|
||||
expect(shellRowSignature(CAPTURES.ctrlz)).toBe(shellRowSignature(CAPTURES.idle))
|
||||
expect(shellRowSignature(CAPTURES.foreground)).not.toBe(shellRowSignature(CAPTURES.idle))
|
||||
|
||||
// Same premise for the `set +m` captures, minus `ppid`: their harness keeps its parent alive
|
||||
// rather than reparenting the shell to init, and the ppid is the one field of the shape the
|
||||
// predicate never reads.
|
||||
const paneShape = (capture: { rootPid: number; table: readonly string[] }): string =>
|
||||
shellShape(capture).split(' ').slice(1).join(' ')
|
||||
expect(paneShape(CAPTURES.setMinusMBackground)).toBe(paneShape(CAPTURES.idle))
|
||||
expect(paneShape(CAPTURES.nottyGroupMember)).toBe(paneShape(CAPTURES.idle))
|
||||
expect(paneShape(CAPTURES.doubleForkedGroupMember)).toBe(paneShape(CAPTURES.idle))
|
||||
// rather than reparenting the shell to init, and the ppid is the one field of the signature
|
||||
// the predicate never reads.
|
||||
const paneRowSignature = (capture: { rootPid: number; table: readonly string[] }): string =>
|
||||
shellRowSignature(capture).split(' ').slice(1).join(' ')
|
||||
expect(paneRowSignature(CAPTURES.setMinusMBackground)).toBe(paneRowSignature(CAPTURES.idle))
|
||||
expect(paneRowSignature(CAPTURES.nottyGroupMember)).toBe(paneRowSignature(CAPTURES.idle))
|
||||
expect(paneRowSignature(CAPTURES.doubleForkedGroupMember)).toBe(paneRowSignature(CAPTURES.idle))
|
||||
})
|
||||
|
||||
it('sweeps an idle shell', async () => {
|
||||
|
||||
@@ -232,9 +232,9 @@ describe('waitForSentinel', () => {
|
||||
|
||||
it.each(['ssh2 channel', 'system-SSH child stdio'])(
|
||||
'forwards write(false), callback settlement, and drain for a %s',
|
||||
async (shape) => {
|
||||
async (channelKind) => {
|
||||
const channel = createMockChannel()
|
||||
if (shape.startsWith('system')) {
|
||||
if (channelKind.startsWith('system')) {
|
||||
Object.assign(channel, { _process: new EventEmitter() })
|
||||
}
|
||||
const callback = vi.fn()
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function detectRemoteHostPlatform(
|
||||
}
|
||||
// Why: only the PowerShell probe can settle a uname the parser cannot map
|
||||
// (Cygwin, say), so a refused or timed-out channel leaves it unsettled.
|
||||
const windowsProbeNeverRan = windows.kind === 'failed' && isTransportShapedError(windows.error)
|
||||
const windowsProbeNeverRan = windows.kind === 'failed' && isTransportFailure(windows.error)
|
||||
if ((uname.kind === 'unsupported' && !windowsProbeNeverRan) || windows.kind === 'unsupported') {
|
||||
const reported = uname.kind === 'unsupported' ? uname.uname : probeUname(windows)
|
||||
console.warn(`[ssh-relay] Remote reported an unsupported platform: ${reported}`)
|
||||
@@ -66,7 +66,7 @@ function undetectedPlatformError(
|
||||
windows: PlatformProbeOutcome
|
||||
): Error {
|
||||
for (const outcome of [uname, windows]) {
|
||||
if (outcome.kind === 'failed' && isTransportShapedError(outcome.error)) {
|
||||
if (outcome.kind === 'failed' && isTransportFailure(outcome.error)) {
|
||||
return wrapProbeError(outcome.error)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ function undetectedPlatformError(
|
||||
|
||||
// Why: a refused or timed-out channel explains the failure better than the
|
||||
// other probe's mundane non-zero exit (e.g. "sh: not found" on Windows).
|
||||
function isTransportShapedError(error: unknown): boolean {
|
||||
function isTransportFailure(error: unknown): boolean {
|
||||
return (
|
||||
isSshSessionLimitError(error) ||
|
||||
isUnconfirmedSshCommandTermination(error) ||
|
||||
|
||||
+1
-1
@@ -327,7 +327,7 @@ describe('generateCommitMessageFromContext', () => {
|
||||
'401: {"message":"slot 1:/Users/name/alt failed"}',
|
||||
'Pi CLI command failed with code 1: 401: {"message":"slot 1:[path] failed"}'
|
||||
]
|
||||
])('redacts a %s in provider bodies', async (_shape, stderr, expected) => {
|
||||
])('redacts a %s in provider bodies', async (_variant, stderr, expected) => {
|
||||
const result = await generateCommitMessageFromContext(
|
||||
{
|
||||
branch: 'main',
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('WSL vault intermediate-symlink reproduction', () => {
|
||||
it.each([
|
||||
['file-shaped', `${FIXTURE_ROOT}/linked-project/session.json`, false],
|
||||
['directory-shaped', `${FIXTURE_ROOT}/linked-project/session`, true]
|
||||
])('rejects a %s target before removal', async (_shape, target, recursive) => {
|
||||
])('rejects a %s target before removal', async (_targetKind, target, recursive) => {
|
||||
const options = { recursive, approvedRoots: [unc(FIXTURE_ROOT)] }
|
||||
let rejection: unknown
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe.skipIf(!runRealWsl)('WSL contained delete integration', () => {
|
||||
it.each([
|
||||
['file-shaped', 'file-link/session.json', false],
|
||||
['directory-shaped', 'dir-link/session', true]
|
||||
])('rejects a %s escape and preserves all outside entries', async (_shape, path, recursive) => {
|
||||
])('rejects a %s escape and preserves all outside entries', async (_label, path, recursive) => {
|
||||
const vaultRoot = `${fixtureRoot}/vault`
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -547,10 +547,10 @@ describe('WSL availability cache', () => {
|
||||
it.each([
|
||||
['wsl.exe reports WSL unusable', { status: 1 }],
|
||||
['wsl.exe is not installed', { code: 'ENOENT' }]
|
||||
])('holds a definitive failure far longer than a timeout when %s', (_label, errorShape) => {
|
||||
])('holds a definitive failure far longer than a timeout when %s', (_label, errorFields) => {
|
||||
vi.useFakeTimers()
|
||||
execFileSyncMock.mockImplementationOnce(() => {
|
||||
throw Object.assign(new Error('definitive failure'), errorShape)
|
||||
throw Object.assign(new Error('definitive failure'), errorFields)
|
||||
})
|
||||
execFileSyncMock.mockReturnValueOnce('')
|
||||
|
||||
@@ -621,10 +621,10 @@ describe('WSL availability cache', () => {
|
||||
it.each([
|
||||
['a definitive failure', { status: 1 }],
|
||||
['a timeout', { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' }]
|
||||
])('re-probes availability once a distro list succeeds after %s', (_label, errorShape) => {
|
||||
])('re-probes availability once a distro list succeeds after %s', (_label, errorFields) => {
|
||||
vi.useFakeTimers()
|
||||
execFileSyncMock.mockImplementationOnce(() => {
|
||||
throw Object.assign(new Error('probe failed'), errorShape)
|
||||
throw Object.assign(new Error('probe failed'), errorFields)
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -96,7 +96,7 @@ const DIFF_ALLOWED_FLAGS = new Set([
|
||||
// only those two exact shapes, held to the same remote-name and URL rules the
|
||||
// relay already enforces on every pushTarget-carrying RPC. Everything else --
|
||||
// set-url, rename, prune, flags before the action -- stays blocked.
|
||||
function isAllowedRemoteWriteShape(args: string[]): boolean {
|
||||
function isAllowedRemoteWriteInvocation(args: string[]): boolean {
|
||||
if (args[1] === 'add') {
|
||||
return args.length === 4 && isSafeGitRemoteName(args[2]) && isSafePushTargetRemoteUrl(args[3])
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export function validateGitExecArgs(args: string[]): void {
|
||||
if (
|
||||
remoteSubcmd &&
|
||||
REMOTE_WRITE_SUBCOMMANDS.has(remoteSubcmd) &&
|
||||
!isAllowedRemoteWriteShape(args)
|
||||
!isAllowedRemoteWriteInvocation(args)
|
||||
) {
|
||||
throw new Error('Destructive git remote operations are not allowed via exec')
|
||||
}
|
||||
|
||||
@@ -128,10 +128,10 @@ describe('pinned and legacy branch diff equivalence against real Git', () => {
|
||||
for (const entry of compare.entries) {
|
||||
// Exactly what the renderer sends: paths from the compare entry list,
|
||||
// OIDs from the compare summary that produced that same list.
|
||||
const callerShape = { filePath: entry.path, oldPath: entry.oldPath }
|
||||
const legacy = await branchDiff(callerShape)
|
||||
const callerParams = { filePath: entry.path, oldPath: entry.oldPath }
|
||||
const legacy = await branchDiff(callerParams)
|
||||
const pinned = await branchDiff({
|
||||
...callerShape,
|
||||
...callerParams,
|
||||
baseRef: compare.summary.mergeBase,
|
||||
headOid: compare.summary.headOid
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { parseBranchDiff } from './git-handler-utils'
|
||||
import { parseNumstat } from '../shared/git-uncommitted-line-stats'
|
||||
import { isNoUpstreamError, normalizeGitErrorMessage } from '../shared/git-remote-error'
|
||||
import { upstreamOnlyCommitsArePatchEquivalent } from '../shared/git-upstream-status'
|
||||
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
|
||||
import { getPublishTargetStatus, type GitCommandRunner } from '../shared/git-publish-target-status'
|
||||
import type { GitPushTarget } from '../shared/worktree/types'
|
||||
import { getEffectiveGitUpstreamStatus } from '../shared/git-effective-upstream'
|
||||
@@ -46,7 +46,7 @@ export class GitHandlerComparisonOperations extends GitHandlerOperationContext {
|
||||
|
||||
try {
|
||||
if (params.pushTarget !== undefined) {
|
||||
assertGitPushTargetShape(params.pushTarget)
|
||||
assertValidGitPushTarget(params.pushTarget)
|
||||
const pushTarget = params.pushTarget as GitPushTarget
|
||||
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
|
||||
return await getPublishTargetStatus(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestContext } from './dispatcher'
|
||||
import { GitHandlerOperationContext } from './git-handler-operation-context'
|
||||
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
|
||||
import type { GitPushTarget } from '../shared/worktree/types'
|
||||
import { normalizeGitErrorMessage, isExecKilledError } from '../shared/git-remote-error'
|
||||
import { syncForkDefaultBranch, validateGitForkSyncExpectedUpstream } from '../shared/git-fork-sync'
|
||||
@@ -21,7 +21,7 @@ export class GitHandlerFetchOperations extends GitHandlerOperationContext {
|
||||
try {
|
||||
try {
|
||||
if (params.pushTarget !== undefined) {
|
||||
assertGitPushTargetShape(params.pushTarget)
|
||||
assertValidGitPushTarget(params.pushTarget)
|
||||
const pushTarget = params.pushTarget as GitPushTarget
|
||||
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
|
||||
await this.git(['fetch', '--prune', pushTarget.remoteName], worktreePath)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
|
||||
import {
|
||||
resolveConfiguredGitPushTarget,
|
||||
type ResolvedGitPushTarget
|
||||
@@ -15,7 +15,7 @@ export async function resolveRelayPushTarget(
|
||||
if (pushTarget === undefined) {
|
||||
return resolveConfiguredGitPushTarget((args) => git(args, worktreePath))
|
||||
}
|
||||
assertGitPushTargetShape(pushTarget)
|
||||
assertValidGitPushTarget(pushTarget)
|
||||
const explicitTarget: GitPushTarget = pushTarget
|
||||
// Why here and not in the shared resolver: an explicit target arrives over the wire,
|
||||
// so the host re-validates its shape and asks Git to vet the branch name itself.
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RequestContext } from './dispatcher'
|
||||
import { GitHandlerOperationContext } from './git-handler-operation-context'
|
||||
import { resolveRelayPushTarget } from './git-handler-push-target'
|
||||
import { normalizeGitErrorMessage, runPullWithDivergenceFallback } from '../shared/git-remote-error'
|
||||
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
|
||||
import type { GitCommandRunner } from '../shared/git-publish-target-status'
|
||||
import type { GitPushTarget } from '../shared/worktree/types'
|
||||
import { resolveEffectiveGitUpstream } from '../shared/git-effective-upstream'
|
||||
@@ -63,7 +63,7 @@ export class GitHandlerSyncOperations extends GitHandlerOperationContext {
|
||||
const worktreePath = params.worktreePath as string
|
||||
const runPull = async (effectiveArgs: string[]): Promise<void> => {
|
||||
if (params.pushTarget !== undefined) {
|
||||
assertGitPushTargetShape(params.pushTarget)
|
||||
assertValidGitPushTarget(params.pushTarget)
|
||||
const pushTarget = params.pushTarget as GitPushTarget
|
||||
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
|
||||
await this.git(
|
||||
|
||||
@@ -149,7 +149,7 @@ function parseStructuredPayload(value: string): HtmlSuperscriptLinkSource | null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (!isCitationShape(candidate)) {
|
||||
if (!isCitationSource(candidate)) {
|
||||
return null
|
||||
}
|
||||
const parsed = parseHtmlSuperscriptLinkSource(candidate.source)
|
||||
@@ -197,7 +197,7 @@ function hasOnlyAttributes(element: Element, allowed: string[]): boolean {
|
||||
return Array.from(element.attributes).every((attribute) => allowedSet.has(attribute.name))
|
||||
}
|
||||
|
||||
function isCitationShape(value: unknown): value is HtmlSuperscriptLinkSource {
|
||||
function isCitationSource(value: unknown): value is HtmlSuperscriptLinkSource {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Palette,
|
||||
Rocket,
|
||||
Server,
|
||||
// `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json.
|
||||
Shapes,
|
||||
Sparkles,
|
||||
SquareTerminal,
|
||||
|
||||
+2
-4
@@ -13,9 +13,7 @@ export type DiscardConfirmationCopy = {
|
||||
* Untracked and newly-added paths have no HEAD version to restore, so Orca's discard removes the
|
||||
* working-tree file. Every surface that names the operation must say "delete" for these.
|
||||
*/
|
||||
export function isDeleteShapedDiscardEntry(
|
||||
entry: Pick<GitStatusEntry, 'area' | 'status'>
|
||||
): boolean {
|
||||
export function discardDeletesEntryFile(entry: Pick<GitStatusEntry, 'area' | 'status'>): boolean {
|
||||
return entry.area === 'untracked' || entry.status === 'untracked' || entry.status === 'added'
|
||||
}
|
||||
|
||||
@@ -24,7 +22,7 @@ export function getDiscardEntryConfirmationCopy(
|
||||
): DiscardConfirmationCopy {
|
||||
const name = basename(entry.path)
|
||||
|
||||
if (isDeleteShapedDiscardEntry(entry)) {
|
||||
if (discardDeletesEntryFile(entry)) {
|
||||
return {
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.discard.confirmation.96c772bee9',
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ describe('showSourceControlEntryFailureToast', () => {
|
||||
it('says "delete" for an entry whose discard removes the file rather than restoring it', () => {
|
||||
// Why: untracked and added paths have no HEAD version, so the row button and the confirmation
|
||||
// dialog both say "delete" — the failure must not contradict the verb the user pressed.
|
||||
show({ operation: 'discard', deleteShaped: true })
|
||||
show({ operation: 'discard', deletesFile: true })
|
||||
expect(lastToast().title).toBe('Failed to delete “src/app.ts”')
|
||||
})
|
||||
|
||||
|
||||
+5
-5
@@ -26,7 +26,7 @@ export function dismissSourceControlEntryFailureToast(worktreeId: string | null)
|
||||
function entryFailureTitle(
|
||||
operation: SourceControlEntryOperation,
|
||||
filePath: string,
|
||||
deleteShaped: boolean
|
||||
deletesFile: boolean
|
||||
): string {
|
||||
switch (operation) {
|
||||
case 'stage':
|
||||
@@ -42,7 +42,7 @@ function entryFailureTitle(
|
||||
{ value0: filePath }
|
||||
)
|
||||
case 'discard':
|
||||
return deleteShaped
|
||||
return deletesFile
|
||||
? translate(
|
||||
'auto.components.right.sidebar.SourceControl.entryDeleteFailed',
|
||||
'Failed to delete “{{value0}}”',
|
||||
@@ -67,7 +67,7 @@ function entryFailureTitle(
|
||||
export function showSourceControlEntryFailureToast({
|
||||
operation,
|
||||
filePath,
|
||||
deleteShaped = false,
|
||||
deletesFile = false,
|
||||
error,
|
||||
worktreeId,
|
||||
worktreeName,
|
||||
@@ -76,7 +76,7 @@ export function showSourceControlEntryFailureToast({
|
||||
operation: SourceControlEntryOperation
|
||||
filePath: string
|
||||
/** True when this discard deletes the file rather than restoring it — see `discard-confirmation`. */
|
||||
deleteShaped?: boolean
|
||||
deletesFile?: boolean
|
||||
error: unknown
|
||||
/** The worktree the failed attempt ran against. */
|
||||
worktreeId: string | null
|
||||
@@ -85,7 +85,7 @@ export function showSourceControlEntryFailureToast({
|
||||
onRetry?: () => void
|
||||
}): void {
|
||||
const isActiveWorktree = useAppStore.getState().activeWorktreeId === worktreeId
|
||||
const title = entryFailureTitle(operation, filePath, deleteShaped)
|
||||
const title = entryFailureTitle(operation, filePath, deletesFile)
|
||||
const offerRetry = Boolean(onRetry) && isActiveWorktree
|
||||
entryFailureSlotOwner = { worktreeId }
|
||||
toast.error(
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import {
|
||||
runDiscardAllForArea,
|
||||
type DiscardAllArea
|
||||
} from './discard-all-sequence'
|
||||
import { isDeleteShapedDiscardEntry } from './discard-confirmation'
|
||||
import { discardDeletesEntryFile } from './discard-confirmation'
|
||||
import { readIpcErrorMessage } from '@/lib/ipc-error'
|
||||
import {
|
||||
dismissSourceControlEntryFailureToast,
|
||||
@@ -62,7 +62,7 @@ export function useSourceControlDiscardConfirmation({
|
||||
showSourceControlEntryFailureToast({
|
||||
operation: 'discard',
|
||||
filePath: entry.path,
|
||||
deleteShaped: isDeleteShapedDiscardEntry(entry),
|
||||
deletesFile: discardDeletesEntryFile(entry),
|
||||
error,
|
||||
worktreeId: activeWorktreeId,
|
||||
worktreeName: worktreePath ? basename(worktreePath) : null
|
||||
|
||||
@@ -223,14 +223,14 @@ export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActio
|
||||
}
|
||||
}
|
||||
|
||||
type CopyShape = {
|
||||
type DaemonActionCopy = {
|
||||
title: string
|
||||
description: React.ReactNode
|
||||
confirmLabel: string
|
||||
busyLabel: string
|
||||
}
|
||||
|
||||
function getCopy(kind: DaemonActionKind): CopyShape {
|
||||
function getCopy(kind: DaemonActionKind): DaemonActionCopy {
|
||||
if (kind === 'restart') {
|
||||
return {
|
||||
title: translate(
|
||||
|
||||
@@ -164,11 +164,11 @@ function exactFileLink(value: string, allowSpacedRelative: boolean): ParsedTermi
|
||||
if (!parsed) {
|
||||
return null
|
||||
}
|
||||
const hasPathShape =
|
||||
const looksLikePath =
|
||||
ROOTED_PATH_PREFIX_PATTERN.test(parsed.pathText) ||
|
||||
/[\\/]/.test(parsed.pathText) ||
|
||||
/\.[\p{L}][\p{L}\p{N}\p{M}_+-]*$/u.test(parsed.pathText)
|
||||
if (!hasPathShape) {
|
||||
if (!looksLikePath) {
|
||||
return null
|
||||
}
|
||||
const explicitLink = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FolderInput,
|
||||
FolderTree,
|
||||
Plus,
|
||||
// `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json.
|
||||
Shapes,
|
||||
SlidersHorizontal,
|
||||
Trash2
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ import { REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS } from './remote-runtime-pty-re
|
||||
const ELECTRON_IPC_PREFIX = "Error invoking remote method 'runtimeEnvironments:call': "
|
||||
|
||||
/** A rejection exactly as the renderer sees it after Electron IPC strips custom props. */
|
||||
function electronIpcShapedRejection(errorName: string, message: string): Error {
|
||||
function electronIpcRejection(errorName: string, message: string): Error {
|
||||
return new Error(`${ELECTRON_IPC_PREFIX}${errorName}: ${message}`)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
|
||||
const { isRecoverableRemoteRuntimeConnectionError, toRemoteRuntimeClientErrorLike } =
|
||||
await import('../../../../shared/remote-runtime-client-error-classification')
|
||||
const rendererSide = toRemoteRuntimeClientErrorLike(
|
||||
electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
|
||||
electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
|
||||
)
|
||||
// Electron IPC stripped the code; the fragment list still catches this one.
|
||||
expect(rendererSide.code).toBeUndefined()
|
||||
@@ -201,7 +201,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
|
||||
// per-selector RPC queue saturated by 15s-timeout calls) is classified
|
||||
// fatal even though its own code says "retry later".
|
||||
const overload = toRemoteRuntimeClientErrorLike(
|
||||
electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
|
||||
electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
|
||||
)
|
||||
expect(overload.code).toBeUndefined()
|
||||
// DESIRED: transient capacity pressure during an outage is recoverable,
|
||||
@@ -220,7 +220,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
|
||||
runtimeCall.mockImplementation(async (request: { method: string; params?: unknown }) => {
|
||||
if (request.method === 'terminal.send') {
|
||||
sendRejections += 1
|
||||
throw electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
|
||||
throw electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
|
||||
}
|
||||
return healthyImpl(request)
|
||||
})
|
||||
@@ -294,7 +294,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
|
||||
if (request.method === 'terminal.resolvePane') {
|
||||
throw Object.assign(new Error(fatalMessage), { code: 'unauthorized' })
|
||||
}
|
||||
throw electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
|
||||
throw electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
|
||||
})
|
||||
subscriptionCallbacks?.onClose?.()
|
||||
await vi.waitFor(() => expect(onError).toHaveBeenCalled())
|
||||
|
||||
@@ -107,7 +107,7 @@ function openTerminalWithSearch(): SearchHarness {
|
||||
* showed up for some of them, so the regression has to sweep rather than pin
|
||||
* one lucky case.
|
||||
*/
|
||||
const CONTENT_SHAPES: readonly (readonly [string, string])[] = [
|
||||
const CONTENT_LAYOUTS: readonly (readonly [string, string])[] = [
|
||||
['matches on two lines', 'needle one\r\nneedle two\r\n'],
|
||||
['matches on three lines', 'needle one\r\nneedle two\r\nneedle three\r\n'],
|
||||
['matches on four lines', 'needle a\r\nneedle b\r\nneedle c\r\nneedle d\r\n'],
|
||||
@@ -128,7 +128,7 @@ describe('terminal search decoration cleanup (STA-2707)', () => {
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
it.each(CONTENT_SHAPES)(
|
||||
it.each(CONTENT_LAYOUTS)(
|
||||
'leaves no highlighted cells after closing search (%s)',
|
||||
async (_name, content) => {
|
||||
// Sweeping the match-navigation count matters: which decoration is the
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getEditorExternalWatchTargetKey,
|
||||
selectEditorExternalWatchTargets,
|
||||
type EditorExternalWatchTarget,
|
||||
type EditorExternalWatchTargetState as EditorExternalWatchTargetStateShape
|
||||
type EditorExternalWatchTargetState
|
||||
} from './editor-external-watch-targets'
|
||||
import {
|
||||
buildEditorExternalWatchEventHandler,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from './editor-external-watch-event-reconciliation'
|
||||
import { verifyLatchedEditorMoveDestinations } from './editor-external-watch-disk-verification'
|
||||
|
||||
export type EditorExternalWatchTargetState = EditorExternalWatchTargetStateShape
|
||||
export type { EditorExternalWatchTargetState }
|
||||
|
||||
function warnExternalWatchFailure(target: EditorExternalWatchTarget, err: unknown): void {
|
||||
console.warn('[filesystem-watch] failed to watch worktree', {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
|
||||
const AGENTS: readonly TuiAgent[] = ['claude', 'codex']
|
||||
const SLOT_COUNT = 7
|
||||
const SHAPE_COUNT = 3 ** SLOT_COUNT * 4 * 2
|
||||
const COMBINATION_COUNT = 3 ** SLOT_COUNT * 4 * 2
|
||||
const TITLES: readonly string[] = ['', 'zsh', 'Task - claude', 'Task - codex']
|
||||
|
||||
type Breakdown = Record<
|
||||
@@ -124,7 +124,7 @@ describe('renderer ladder decision table', () => {
|
||||
const proofFree = runDecisionTable(false)
|
||||
const freshProof = runDecisionTable(true)
|
||||
const result = {
|
||||
shapes: SHAPE_COUNT,
|
||||
combinations: COMBINATION_COUNT,
|
||||
proofOmitted: proofFree,
|
||||
freshProof,
|
||||
flippedByAddingProof: proofFree.flipped
|
||||
|
||||
@@ -131,7 +131,7 @@ export type FocusedPaneCensus = {
|
||||
|
||||
type CountableRecord = Record<string, unknown> | null | undefined
|
||||
|
||||
export type TypingCensusStoreShape = {
|
||||
export type TypingCensusStoreView = {
|
||||
worktreesByRepo?: Record<string, WorktreeLike[]> | null
|
||||
tabsByWorktree?: Record<string, unknown[]> | null
|
||||
unifiedTabsByWorktree?: Record<string, unknown[]> | null
|
||||
@@ -231,7 +231,7 @@ function collectWorktrees(
|
||||
}
|
||||
|
||||
export function summarizeTypingScaleCensus(input: {
|
||||
state: TypingCensusStoreShape | null
|
||||
state: TypingCensusStoreView | null
|
||||
appVersion: string | null
|
||||
livePaneCount: number | null
|
||||
instrumentedPaneCount: number
|
||||
|
||||
@@ -67,11 +67,11 @@ function buildCase(random: () => number): {
|
||||
worktreesByRepo: Record<string, readonly OwnerRecord[]>
|
||||
probeIds: string[]
|
||||
} {
|
||||
const shape = random()
|
||||
if (shape < 0.05) {
|
||||
const roll = random()
|
||||
if (roll < 0.05) {
|
||||
return { detectedWorktreesByRepo: undefined, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
|
||||
}
|
||||
if (shape < 0.1) {
|
||||
if (roll < 0.1) {
|
||||
return { detectedWorktreesByRepo: {}, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
|
||||
}
|
||||
const repoCount = 1 + Math.floor(random() * 6)
|
||||
|
||||
@@ -65,19 +65,19 @@ function makeEntry(index: number, overrides: Record<string, unknown> = {}): neve
|
||||
}
|
||||
|
||||
describe('mobile agent-status projection equivalence', () => {
|
||||
it('matches the whole-array serialization across shapes and cache reuse', () => {
|
||||
it('matches the whole-array serialization across status maps and cache reuse', () => {
|
||||
resetRuntimeMobileAgentStatusProjectionCacheForTests()
|
||||
const shapes: AppState['agentStatusByPaneKey'][] = []
|
||||
shapes.push({})
|
||||
shapes.push({ 'tab-0:leaf-0': makeEntry(0) })
|
||||
shapes.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) })
|
||||
const statusMaps: AppState['agentStatusByPaneKey'][] = []
|
||||
statusMaps.push({})
|
||||
statusMaps.push({ 'tab-0:leaf-0': makeEntry(0) })
|
||||
statusMaps.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) })
|
||||
const many: AppState['agentStatusByPaneKey'] = {}
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
many[`tab-${index}:leaf-0`] = makeEntry(index)
|
||||
}
|
||||
shapes.push(many)
|
||||
statusMaps.push(many)
|
||||
// Optional fields absent entirely, which the ?? null fallbacks must cover.
|
||||
shapes.push({
|
||||
statusMaps.push({
|
||||
'tab-9:leaf-1': makeEntry(9, {
|
||||
agentType: undefined,
|
||||
terminalTitle: undefined,
|
||||
@@ -89,17 +89,17 @@ describe('mobile agent-status projection equivalence', () => {
|
||||
})
|
||||
})
|
||||
// Keys deliberately out of insertion order to pin the sort.
|
||||
shapes.push({
|
||||
statusMaps.push({
|
||||
'tab-z:leaf-0': makeEntry(2),
|
||||
'tab-a:leaf-0': makeEntry(1),
|
||||
'tab-m:leaf-0': makeEntry(3)
|
||||
})
|
||||
|
||||
for (const [index, shape] of shapes.entries()) {
|
||||
for (const [index, statusMap] of statusMaps.entries()) {
|
||||
expect({
|
||||
index,
|
||||
projection: buildRuntimeMobileAgentStatusProjectionForTests(shape)
|
||||
}).toEqual({ index, projection: referenceProjection(shape) })
|
||||
projection: buildRuntimeMobileAgentStatusProjectionForTests(statusMap)
|
||||
}).toEqual({ index, projection: referenceProjection(statusMap) })
|
||||
}
|
||||
|
||||
// Now exercise the cache: replace one entry the way setAgentStatus does and
|
||||
|
||||
@@ -256,7 +256,7 @@ describe('editor draft projection on the typing path', () => {
|
||||
})
|
||||
|
||||
it('matches the uncached projection byte for byte across draft shapes', () => {
|
||||
const shapes: Record<string, string>[] = [
|
||||
const draftCases: Record<string, string>[] = [
|
||||
{},
|
||||
{ 'file-a': '' },
|
||||
{ 'file-a': 'hello' },
|
||||
@@ -267,10 +267,10 @@ describe('editor draft projection on the typing path', () => {
|
||||
{ 'file-a': 'hello', 'file-b': 'world', 'file-c': 'third' },
|
||||
{ 'file-a': 'HELLO', 'file-c': 'third' }
|
||||
]
|
||||
for (const [index, shape] of shapes.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(shape) }).toEqual({
|
||||
for (const [index, draft] of draftCases.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(draft) }).toEqual({
|
||||
index,
|
||||
projection: referenceEditorDraftsProjection(shape)
|
||||
projection: referenceEditorDraftsProjection(draft)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -402,7 +402,7 @@ describe('open-files and browser projections', () => {
|
||||
})
|
||||
|
||||
it('matches the uncached projections byte for byte across shapes', () => {
|
||||
const openFileShapes: AppState['openFiles'][] = [
|
||||
const openFileCases: AppState['openFiles'][] = [
|
||||
[] as unknown as AppState['openFiles'],
|
||||
[makeOpenFile(0)] as unknown as AppState['openFiles'],
|
||||
[makeOpenFile(0, { isDirty: true })] as unknown as AppState['openFiles'],
|
||||
@@ -412,14 +412,14 @@ describe('open-files and browser projections', () => {
|
||||
makeOpenFile(2, { isUntitled: true, deleteUntouchedOnClose: true, language: undefined })
|
||||
] as unknown as AppState['openFiles']
|
||||
]
|
||||
for (const [index, shape] of openFileShapes.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileOpenFilesProjection(shape) }).toEqual({
|
||||
for (const [index, openFiles] of openFileCases.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileOpenFilesProjection(openFiles) }).toEqual({
|
||||
index,
|
||||
projection: referenceOpenFilesProjection(shape)
|
||||
projection: referenceOpenFilesProjection(openFiles)
|
||||
})
|
||||
}
|
||||
|
||||
const browserShapes: AppState[] = [
|
||||
const browserCases: AppState[] = [
|
||||
makeState({}),
|
||||
makeState({ browserTabsByWorktree: { 'wt-1': [makeBrowserWorkspace(0)] } as never }),
|
||||
makeState({
|
||||
@@ -437,10 +437,10 @@ describe('open-files and browser projections', () => {
|
||||
browserPagesByWorkspace: { 'ws-9': [makeBrowserPage(9, { url: 'a"b\\c' })] } as never
|
||||
})
|
||||
]
|
||||
for (const [index, shape] of browserShapes.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileBrowserProjection(shape) }).toEqual({
|
||||
for (const [index, state] of browserCases.entries()) {
|
||||
expect({ index, projection: buildRuntimeMobileBrowserProjection(state) }).toEqual({
|
||||
index,
|
||||
projection: referenceBrowserProjection(shape)
|
||||
projection: referenceBrowserProjection(state)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,13 +30,17 @@ type UsageSnapshot = {
|
||||
recentSessions: object[]
|
||||
}
|
||||
|
||||
type UsageShape<Scope extends string, Range extends string, Snapshot extends UsageSnapshot> = {
|
||||
type UsageProviderTypes<
|
||||
Scope extends string,
|
||||
Range extends string,
|
||||
Snapshot extends UsageSnapshot
|
||||
> = {
|
||||
scope: Scope
|
||||
range: Range
|
||||
snapshot: Snapshot
|
||||
}
|
||||
|
||||
type UsageData<T extends UsageShape<string, string, UsageSnapshot>> = {
|
||||
type UsageData<T extends UsageProviderTypes<string, string, UsageSnapshot>> = {
|
||||
scope: T['scope']
|
||||
range: T['range']
|
||||
scanState: T['snapshot']['scanState'] | null
|
||||
@@ -47,7 +51,7 @@ type UsageData<T extends UsageShape<string, string, UsageSnapshot>> = {
|
||||
recentSessions: T['snapshot']['recentSessions']
|
||||
}
|
||||
|
||||
type UsageApi<T extends UsageShape<string, string, UsageSnapshot>> = {
|
||||
type UsageApi<T extends UsageProviderTypes<string, string, UsageSnapshot>> = {
|
||||
getScanState: () => Promise<T['snapshot']['scanState']>
|
||||
setEnabled: (args: { enabled: boolean }) => Promise<T['snapshot']['scanState']>
|
||||
refresh: (args?: { force?: boolean }) => Promise<T['snapshot']['scanState']>
|
||||
@@ -61,7 +65,7 @@ type UsageApi<T extends UsageShape<string, string, UsageSnapshot>> = {
|
||||
type ProviderUsageSlice<
|
||||
Prefix extends string,
|
||||
Name extends string,
|
||||
T extends UsageShape<string, string, UsageSnapshot>
|
||||
T extends UsageProviderTypes<string, string, UsageSnapshot>
|
||||
> = {
|
||||
[K in keyof UsageData<T> as `${Prefix}Usage${Capitalize<K & string>}`]: UsageData<T>[K]
|
||||
} & Record<`set${Name}UsageEnabled`, (enabled: boolean) => Promise<void>> &
|
||||
@@ -74,7 +78,7 @@ type ProviderUsageSlice<
|
||||
type UsageProviderConfig<
|
||||
Prefix extends string,
|
||||
Name extends string,
|
||||
T extends UsageShape<string, string, UsageSnapshot>
|
||||
T extends UsageProviderTypes<string, string, UsageSnapshot>
|
||||
> = {
|
||||
prefix: Prefix
|
||||
name: Name
|
||||
@@ -93,13 +97,13 @@ const usageDataFields = [
|
||||
'modelBreakdown',
|
||||
'projectBreakdown',
|
||||
'recentSessions'
|
||||
] as const satisfies readonly (keyof UsageData<UsageShape<string, string, UsageSnapshot>>)[]
|
||||
] as const satisfies readonly (keyof UsageData<UsageProviderTypes<string, string, UsageSnapshot>>)[]
|
||||
|
||||
function usageDataKey(prefix: string, field: string): string {
|
||||
return `${prefix}Usage${field[0].toUpperCase()}${field.slice(1)}`
|
||||
}
|
||||
|
||||
function readUsageData<T extends UsageShape<string, string, UsageSnapshot>>(
|
||||
function readUsageData<T extends UsageProviderTypes<string, string, UsageSnapshot>>(
|
||||
state: AppState,
|
||||
prefix: string
|
||||
): UsageData<T> {
|
||||
@@ -109,7 +113,7 @@ function readUsageData<T extends UsageShape<string, string, UsageSnapshot>>(
|
||||
) as UsageData<T>
|
||||
}
|
||||
|
||||
function createUsagePatch<T extends UsageShape<string, string, UsageSnapshot>>(
|
||||
function createUsagePatch<T extends UsageProviderTypes<string, string, UsageSnapshot>>(
|
||||
prefix: string,
|
||||
patch: Partial<UsageData<T>>
|
||||
): Partial<AppState> {
|
||||
@@ -123,7 +127,7 @@ function createUsagePatch<T extends UsageShape<string, string, UsageSnapshot>>(
|
||||
function createUsageProviderSlice<
|
||||
Prefix extends string,
|
||||
Name extends string,
|
||||
T extends UsageShape<string, string, UsageSnapshot>
|
||||
T extends UsageProviderTypes<string, string, UsageSnapshot>
|
||||
>(
|
||||
config: UsageProviderConfig<Prefix, Name, T>
|
||||
): StateCreator<AppState, [], [], ProviderUsageSlice<Prefix, Name, T>> {
|
||||
@@ -255,18 +259,22 @@ function createUsageProviderSlice<
|
||||
}
|
||||
}
|
||||
|
||||
type ClaudeUsageShape = UsageShape<ClaudeUsageScope, ClaudeUsageRange, ClaudeUsageSnapshot>
|
||||
type CodexUsageShape = UsageShape<CodexUsageScope, CodexUsageRange, CodexUsageSnapshot>
|
||||
type OpenCodeUsageShape = UsageShape<OpenCodeUsageScope, OpenCodeUsageRange, OpenCodeUsageSnapshot>
|
||||
type ClaudeUsageTypes = UsageProviderTypes<ClaudeUsageScope, ClaudeUsageRange, ClaudeUsageSnapshot>
|
||||
type CodexUsageTypes = UsageProviderTypes<CodexUsageScope, CodexUsageRange, CodexUsageSnapshot>
|
||||
type OpenCodeUsageTypes = UsageProviderTypes<
|
||||
OpenCodeUsageScope,
|
||||
OpenCodeUsageRange,
|
||||
OpenCodeUsageSnapshot
|
||||
>
|
||||
|
||||
export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageShape>
|
||||
export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageShape>
|
||||
export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageShape>
|
||||
export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageTypes>
|
||||
export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageTypes>
|
||||
export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageTypes>
|
||||
|
||||
export const createClaudeUsageSlice = createUsageProviderSlice<
|
||||
'claude',
|
||||
'Claude',
|
||||
ClaudeUsageShape
|
||||
ClaudeUsageTypes
|
||||
>({
|
||||
prefix: 'claude',
|
||||
name: 'Claude',
|
||||
@@ -276,7 +284,7 @@ export const createClaudeUsageSlice = createUsageProviderSlice<
|
||||
hasCachedData: (state) => state.hasAnyClaudeData
|
||||
})
|
||||
|
||||
export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageShape>({
|
||||
export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageTypes>({
|
||||
prefix: 'codex',
|
||||
name: 'Codex',
|
||||
initialScope: 'orca',
|
||||
@@ -288,7 +296,7 @@ export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex',
|
||||
export const createOpenCodeUsageSlice = createUsageProviderSlice<
|
||||
'openCode',
|
||||
'OpenCode',
|
||||
OpenCodeUsageShape
|
||||
OpenCodeUsageTypes
|
||||
>({
|
||||
prefix: 'openCode',
|
||||
name: 'OpenCode',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isSkillsCliAgentKeyShaped } from './skills-cli-agent-keys'
|
||||
import { isUsableSkillsCliAgentKey } from './skills-cli-agent-keys'
|
||||
|
||||
export const ORCA_SKILLS_REPOSITORY_URL = 'https://github.com/stablyai/orca'
|
||||
|
||||
@@ -35,7 +35,7 @@ export function buildAgentFeatureSkillInstallArgs(
|
||||
}
|
||||
// Why: a value the skills CLI would drop leaves it with no target at all, which
|
||||
// is the same all-agents install as passing no --agent.
|
||||
const unusable = agents.find((agent) => !isSkillsCliAgentKeyShaped(agent))
|
||||
const unusable = agents.find((agent) => !isUsableSkillsCliAgentKey(agent))
|
||||
if (unusable !== undefined) {
|
||||
throw new Error(`"${unusable}" is not a usable install target.`)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const SHELLS: { platform: NodeJS.Platform; shell: AgentStartupShell }[] = [
|
||||
/** Independent selector oracle — deliberately NOT the implementation's own
|
||||
* predicate, so a regression that shrinks the stripped set cannot also blind
|
||||
* this assertion. */
|
||||
function isSelectorShapedToken(token: string): boolean {
|
||||
function isResumeSelectorToken(token: string): boolean {
|
||||
return (
|
||||
['--resume', '--continue', '-r', '-c'].includes(token) ||
|
||||
['--resume=', '--continue=', '-r=', '-c='].some((prefix) => token.startsWith(prefix))
|
||||
@@ -31,7 +31,7 @@ function expectSingleAuthoritativeResume(command: string, shell: AgentStartupShe
|
||||
if (!tokenized.ok) {
|
||||
return
|
||||
}
|
||||
const selectors = tokenized.tokens.filter(isSelectorShapedToken)
|
||||
const selectors = tokenized.tokens.filter(isResumeSelectorToken)
|
||||
expect(selectors).toEqual(['--resume'])
|
||||
const index = tokenized.tokens.indexOf('--resume')
|
||||
expect(tokenized.tokens[index + 1]).toBe(SESSION_ID)
|
||||
|
||||
@@ -242,7 +242,7 @@ export function isAdmissibleAgentJournalSubmission(
|
||||
* never reject a row a writer in this build produced. The schemas are
|
||||
* deliberately wider on open string fields, so only this direction holds. */
|
||||
type Admits<T extends true> = T
|
||||
export type CanonicalJournalShapesAreAdmissible = [
|
||||
export type CanonicalJournalTypesAreAdmissible = [
|
||||
Admits<AgentJournalItemBody extends z.input<typeof AgentJournalItemBodySchema> ? true : false>,
|
||||
Admits<AgentJournalMessageItem extends z.input<typeof MessageBody> ? true : false>,
|
||||
Admits<
|
||||
|
||||
@@ -337,7 +337,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor
|
||||
return false
|
||||
}
|
||||
const record = value as Partial<AgentSessionRecord>
|
||||
const shapeValid =
|
||||
const fieldsValid =
|
||||
record.schemaVersion === AGENT_SESSION_RECORD_SCHEMA_VERSION &&
|
||||
isAgentSessionId(record.sessionId) &&
|
||||
isAgentSessionExecutionLocation(record.location) &&
|
||||
@@ -356,7 +356,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor
|
||||
record.lease.sessionId === record.sessionId &&
|
||||
Number.isSafeInteger(record.createdAt) &&
|
||||
Number.isSafeInteger(record.updatedAt)
|
||||
if (!shapeValid) {
|
||||
if (!fieldsValid) {
|
||||
return false
|
||||
}
|
||||
const validated = record as AgentSessionRecord
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { assertGitPushTargetShape } from './git-push-target-validation'
|
||||
import { assertValidGitPushTarget } from './git-push-target-validation'
|
||||
|
||||
describe('assertGitPushTargetShape', () => {
|
||||
describe('assertValidGitPushTarget', () => {
|
||||
it('accepts slash-separated git remote names', () => {
|
||||
expect(() =>
|
||||
assertGitPushTargetShape({ remoteName: 'foo/bar', branchName: 'feature/fix' })
|
||||
assertValidGitPushTarget({ remoteName: 'foo/bar', branchName: 'feature/fix' })
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects remote names with empty or parent segments', () => {
|
||||
expect(() =>
|
||||
assertGitPushTargetShape({ remoteName: 'foo//bar', branchName: 'feature/fix' })
|
||||
assertValidGitPushTarget({ remoteName: 'foo//bar', branchName: 'feature/fix' })
|
||||
).toThrow('Invalid git remote name')
|
||||
expect(() =>
|
||||
assertGitPushTargetShape({ remoteName: 'foo/../bar', branchName: 'feature/fix' })
|
||||
assertValidGitPushTarget({ remoteName: 'foo/../bar', branchName: 'feature/fix' })
|
||||
).toThrow('Invalid git remote name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ export function isSafePushTargetRemoteUrl(remoteUrl: string): boolean {
|
||||
return GITHUB_CLONE_URL.test(remoteUrl) || GITHUB_SSH_URL.test(remoteUrl)
|
||||
}
|
||||
|
||||
export function assertGitPushTargetShape(target: unknown): asserts target is GitPushTarget {
|
||||
export function assertValidGitPushTarget(target: unknown): asserts target is GitPushTarget {
|
||||
if (typeof target !== 'object' || target === null) {
|
||||
throw new Error('Invalid PR push target.')
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function registerQuestionTool(toolName: string, parser: InteractiveQuesti
|
||||
QUESTION_TOOL_PARSERS.set(toolName, parser)
|
||||
}
|
||||
|
||||
function parseQuestionsShape(input: unknown): AskPrompt | null {
|
||||
function parseCanonicalQuestionsInput(input: unknown): AskPrompt | null {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return null
|
||||
}
|
||||
@@ -73,12 +73,12 @@ function parseOptions(raw: unknown): AskOption[] {
|
||||
}
|
||||
|
||||
for (const name of ['AskUserQuestion', 'ask_user_question', 'askUserQuestion']) {
|
||||
QUESTION_TOOL_PARSERS.set(name, parseQuestionsShape)
|
||||
QUESTION_TOOL_PARSERS.set(name, parseCanonicalQuestionsInput)
|
||||
}
|
||||
|
||||
function parseToolInput(toolName: string | undefined, input: unknown): AskPrompt | null {
|
||||
const parser = toolName ? QUESTION_TOOL_PARSERS.get(toolName) : undefined
|
||||
return (parser ? parser(input) : null) ?? parseQuestionsShape(input)
|
||||
return (parser ? parser(input) : null) ?? parseCanonicalQuestionsInput(input)
|
||||
}
|
||||
|
||||
export function parseAskFromStatus(
|
||||
|
||||
@@ -9,6 +9,8 @@ export type OnboardingChecklistState = {
|
||||
ranFirstAgent: boolean
|
||||
ranSecondAgentOnSameTask: boolean
|
||||
triedCmdJ: boolean
|
||||
// Persisted field, also a telemetry enum member in ./telemetry-onboarding-foundation-schemas;
|
||||
// renaming it would orphan saved state. Rule exemption: config/oxlint-anti-slop.json.
|
||||
shapedSidebar: boolean
|
||||
reviewedDiff: boolean
|
||||
openedPr: boolean
|
||||
|
||||
@@ -52,8 +52,8 @@ describe('resolvePaneAgentIdentity', () => {
|
||||
})
|
||||
|
||||
describe('run generation separates the bug from the legitimate reclaim', () => {
|
||||
// Both shapes are `completed hook = A, title = B`. Ordering alone cannot tell them apart.
|
||||
const shape = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [
|
||||
// Both cases are `completed hook = A, title = B`. Ordering alone cannot tell them apart.
|
||||
const evidenceFor = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [
|
||||
{ source: 'completed-hook', agent: 'claude', run: { authorityId: H, incarnation: hookRun } },
|
||||
{ source: 'title', agent: 'codex', run: { authorityId: H, incarnation: titleRun } }
|
||||
]
|
||||
@@ -61,7 +61,7 @@ describe('resolvePaneAgentIdentity', () => {
|
||||
it('keeps the completed hook when both belong to the current run', () => {
|
||||
// The reported bug: nothing new started, so the hook is still the truth.
|
||||
const result = resolvePaneAgentIdentity({
|
||||
evidence: shape(7, 7),
|
||||
evidence: evidenceFor(7, 7),
|
||||
currentRun: { authorityId: H, incarnation: 7 }
|
||||
})
|
||||
expect(result).toMatchObject({ agent: 'claude', source: 'completed-hook' })
|
||||
@@ -72,7 +72,7 @@ describe('resolvePaneAgentIdentity', () => {
|
||||
// The legitimate reclaim: the pane was reused, so run 7's hook describes an agent that is
|
||||
// no longer there. It is ineligible, not merely outranked.
|
||||
const result = resolvePaneAgentIdentity({
|
||||
evidence: shape(7, 8),
|
||||
evidence: evidenceFor(7, 8),
|
||||
currentRun: { authorityId: H, incarnation: 8 }
|
||||
})
|
||||
expect(result).toMatchObject({ agent: 'codex', source: 'title' })
|
||||
@@ -82,11 +82,11 @@ describe('resolvePaneAgentIdentity', () => {
|
||||
it('produces opposite answers from identical evidence, given only the run ids', () => {
|
||||
// The whole point, stated as one assertion.
|
||||
const bug = resolvePaneAgentIdentity({
|
||||
evidence: shape(7, 7),
|
||||
evidence: evidenceFor(7, 7),
|
||||
currentRun: { authorityId: H, incarnation: 7 }
|
||||
})
|
||||
const reclaim = resolvePaneAgentIdentity({
|
||||
evidence: shape(7, 8),
|
||||
evidence: evidenceFor(7, 8),
|
||||
currentRun: { authorityId: H, incarnation: 8 }
|
||||
})
|
||||
expect(bug.agent).not.toBe(reclaim.agent)
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH,
|
||||
PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES,
|
||||
validatePluginLanguagePackCatalog,
|
||||
validatePluginLanguagePackCatalogShape,
|
||||
checkPluginLanguagePackCatalog,
|
||||
pluginLanguageResourceId
|
||||
} from './plugin-language-pack-artifact'
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('plugin language-pack artifacts', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(validatePluginLanguagePackCatalogShape(catalog)).toEqual({
|
||||
expect(checkPluginLanguagePackCatalog(catalog)).toEqual({
|
||||
ok: true,
|
||||
entries: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ export function isPluginLanguagePackRegistration(
|
||||
pack.resourceLanguage === pluginLanguageResourceId(pack.id as `plugin:${string}`) &&
|
||||
typeof pack.pluginKey === 'string' &&
|
||||
typeof pack.locale === 'string' &&
|
||||
validatePluginLanguagePackCatalogShape(pack.catalog).ok
|
||||
checkPluginLanguagePackCatalog(pack.catalog).ok
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export function validatePluginLanguagePackCatalog(source: unknown): PluginLangua
|
||||
return { ok: true, catalog: result.catalog!, entries: result.entries }
|
||||
}
|
||||
|
||||
export function validatePluginLanguagePackCatalogShape(
|
||||
export function checkPluginLanguagePackCatalog(
|
||||
source: unknown
|
||||
): PluginLanguagePackValidationResult {
|
||||
const result = walkPluginLanguagePackCatalog(source, false)
|
||||
|
||||
@@ -31,7 +31,7 @@ function isNonNegativeSafeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) >= 0
|
||||
}
|
||||
|
||||
function hasValidRuntimeStatusShape(status: Record<string, unknown>): boolean {
|
||||
function hasValidRuntimeStatus(status: Record<string, unknown>): boolean {
|
||||
return (
|
||||
typeof status.runtimeId === 'string' &&
|
||||
status.runtimeId.length > 0 &&
|
||||
@@ -103,7 +103,7 @@ export function verifyRemotePairingRuntimeStatus(
|
||||
: 'Update Orca on the remote host before adding it.'
|
||||
}
|
||||
}
|
||||
if (!hasValidRuntimeStatusShape(status)) {
|
||||
if (!hasValidRuntimeStatus(status)) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'connection-interrupted',
|
||||
|
||||
@@ -34,12 +34,14 @@ export const RepoUpstream = z
|
||||
.nullable()
|
||||
.optional()
|
||||
|
||||
// The return type is inferred on purpose: an explicit z.ZodObject<...z.ZodRawShape>
|
||||
// annotation widened `updates` to an open record, which erased all 24 named fields
|
||||
// from RpcParams<'repo.update'> for every typed caller.
|
||||
export function createRepoUpdateSchema<T extends z.ZodRawShape>(selectorShape: T) {
|
||||
// The return type is inferred on purpose: an explicit z.ZodObject<...> annotation
|
||||
// widened `updates` to an open record, which erased all 24 named fields from
|
||||
// RpcParams<'repo.update'> for every typed caller.
|
||||
export function createRepoUpdateSchema<T extends Readonly<Record<string, z.ZodType>>>(
|
||||
selectorFields: T
|
||||
) {
|
||||
return z.object({
|
||||
...selectorShape,
|
||||
...selectorFields,
|
||||
updates: z.object({
|
||||
displayName: OptionalString,
|
||||
badgeColor: RepoBadgeColor,
|
||||
|
||||
@@ -21,15 +21,15 @@ type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
||||
/** zod's own input-side key-optionality rule, copied from $InferObjectInput. */
|
||||
type SendOptionalSchema = { _zod: { optin: 'optional' | 'defaulted' } }
|
||||
|
||||
type SendShape<Shape> = Prettify<
|
||||
type SendFields<Fields> = Prettify<
|
||||
{
|
||||
-readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? never : K]: RpcSendInput<
|
||||
Shape[K]
|
||||
-readonly [K in keyof Fields as Fields[K] extends SendOptionalSchema ? never : K]: RpcSendInput<
|
||||
Fields[K]
|
||||
>
|
||||
} & {
|
||||
-readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? K : never]?: RpcSendInput<
|
||||
Shape[K]
|
||||
>
|
||||
-readonly [
|
||||
K in keyof Fields as Fields[K] extends SendOptionalSchema ? K : never
|
||||
]?: RpcSendInput<Fields[K]>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -50,11 +50,11 @@ export type RpcSendInput<Schema> =
|
||||
? RpcSendInput<Element>[]
|
||||
: // ZodObject is the only schema carrying a `shape`, and matching on it keeps
|
||||
// .strict()/.extend()/.superRefine() results in this branch.
|
||||
Schema extends { shape: infer Shape }
|
||||
? keyof Shape extends never
|
||||
Schema extends { shape: infer Fields }
|
||||
? keyof Fields extends never
|
||||
? // Mirrors $InferObjectOutput: a no-field object admits no properties.
|
||||
Record<string, never>
|
||||
: SendShape<Shape>
|
||||
: SendFields<Fields>
|
||||
: // ZodDiscriminatedUnion extends ZodUnion, so both land here.
|
||||
Schema extends z.ZodUnion<infer Options>
|
||||
? RpcSendInput<Options[number]>
|
||||
|
||||
@@ -7,13 +7,15 @@ import type { z } from 'zod'
|
||||
* dropped from the payload and the rest of the batch still lands. Unknown KEYS
|
||||
* stay a hard rejection — the parity assertions exist to catch those.
|
||||
*/
|
||||
export function tolerateUnknownValues<TShape extends z.ZodRawShape>(shape: TShape): TShape {
|
||||
return Object.fromEntries(
|
||||
Object.entries(shape).map(([key, schema]) => [
|
||||
key,
|
||||
(schema as z.ZodType).catch(() => undefined)
|
||||
])
|
||||
) as unknown as TShape
|
||||
export function tolerateUnknownValues<TFields extends Readonly<Record<string, z.ZodType>>>(
|
||||
fields: TFields
|
||||
): TFields {
|
||||
const tolerant: Record<string, z.ZodType> = {}
|
||||
for (const [key, schema] of Object.entries(fields)) {
|
||||
tolerant[key] = schema.catch(() => undefined)
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the loop copies every key of `fields` and only wraps its schema in `.catch()`, so the result carries exactly `TFields`' keys; Object.entries erases that key identity.
|
||||
return tolerant as TFields
|
||||
}
|
||||
|
||||
/** Drops the `undefined` entries `tolerateUnknownValues` leaves behind, so a
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import {
|
||||
SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT,
|
||||
isSkillsCliAgentKeyShaped,
|
||||
isUsableSkillsCliAgentKey,
|
||||
SKILLS_CLI_UNIVERSAL_AGENT_KEY,
|
||||
toSkillsCliAgentKeys
|
||||
} from './skills-cli-agent-keys'
|
||||
@@ -117,10 +117,10 @@ describe('skills CLI agent keys', () => {
|
||||
|
||||
it('rejects values the skills CLI would drop, and allows the explicit wildcard', () => {
|
||||
for (const bad of ['-y', '--copy', '', ' ', 'a b', 'a,b']) {
|
||||
expect(isSkillsCliAgentKeyShaped(bad), bad).toBe(false)
|
||||
expect(isUsableSkillsCliAgentKey(bad), bad).toBe(false)
|
||||
}
|
||||
for (const good of ['claude-code', 'universal', 'trae-cn', 'inference-sh', '*']) {
|
||||
expect(isSkillsCliAgentKeyShaped(good), good).toBe(true)
|
||||
expect(isUsableSkillsCliAgentKey(good), good).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const SKILLS_CLI_UNIVERSAL_AGENT_KEY = 'universal'
|
||||
* emptiness. An unknown-but-plausible key is left to the CLI, which rejects it
|
||||
* loudly with its own valid list before writing anything.
|
||||
*/
|
||||
export function isSkillsCliAgentKeyShaped(value: string): boolean {
|
||||
export function isUsableSkillsCliAgentKey(value: string): boolean {
|
||||
return /^(?:\*|[a-z0-9][a-z0-9.-]*)$/i.test(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,32 +8,32 @@ export type EventName = keyof EventMap
|
||||
export type EventProps<N extends EventName> = EventMap[N]
|
||||
|
||||
// Why: non-`ZodObject` schemas have no `.shape`; return null so `key in undefined` can't throw at module load.
|
||||
function eventSchemaShape(schema: z.ZodTypeAny): z.ZodRawShape | null {
|
||||
// Why `object` and not zod's own field-record type: callers only ask `key in fields`.
|
||||
function eventSchemaFields(schema: z.ZodTypeAny): object | null {
|
||||
if (schema instanceof z.ZodObject) {
|
||||
return schema.shape
|
||||
}
|
||||
|
||||
const shapeBearingSchema = schema as { shape?: unknown }
|
||||
// Why: refined object schemas may expose `.shape` even when refinement breaks `instanceof ZodObject`.
|
||||
if (shapeBearingSchema.shape && typeof shapeBearingSchema.shape === 'object') {
|
||||
return shapeBearingSchema.shape as z.ZodRawShape
|
||||
if ('shape' in schema && typeof schema.shape === 'object' && schema.shape !== null) {
|
||||
return schema.shape
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function eventsWithShapeKey(key: string): ReadonlySet<EventName> {
|
||||
function eventsDeclaringKey(key: string): ReadonlySet<EventName> {
|
||||
return new Set(
|
||||
(Object.entries(eventSchemas) as [EventName, z.ZodTypeAny][])
|
||||
.filter(([, schema]) => {
|
||||
const shape = eventSchemaShape(schema)
|
||||
return shape !== null && key in shape
|
||||
const fields = eventSchemaFields(schema)
|
||||
return fields !== null && key in fields
|
||||
})
|
||||
.map(([name]) => name)
|
||||
)
|
||||
}
|
||||
|
||||
// Cohort injection is gated on this derived set because `.strict()` schemas drop events that don't declare `nth_repo_added`.
|
||||
const COHORT_EXTENDED_SET = eventsWithShapeKey('nth_repo_added')
|
||||
const COHORT_EXTENDED_SET = eventsDeclaringKey('nth_repo_added')
|
||||
|
||||
// Compile-time roster guarding the runtime injection set against silent schema drift.
|
||||
type _CohortExtendedRoster =
|
||||
@@ -78,7 +78,7 @@ export function isCohortExtendedEvent(name: EventName): boolean {
|
||||
}
|
||||
|
||||
// Events whose schema declares `cohort`: the IPC handler injects cohort only for these — a `.strict()` schema without it would reject the event.
|
||||
const ONBOARDING_COHORT_SET = eventsWithShapeKey('cohort')
|
||||
const ONBOARDING_COHORT_SET = eventsDeclaringKey('cohort')
|
||||
// `NonNullable` strips `undefined` introduced by `cohortSchema`'s `.optional()`.
|
||||
export type OnboardingCohort = NonNullable<z.infer<typeof cohortSchema>>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const CONTAINERS: [string, () => z.ZodType, unknown][] = [
|
||||
['salvagingArray', () => salvagingArray(z.string()), ['v']]
|
||||
]
|
||||
|
||||
describe('salvaging containers used bare in an object shape', () => {
|
||||
describe('salvaging containers used bare in an object schema', () => {
|
||||
it.each(CONTAINERS)('%s is neither optional-in nor optional-out', (_name, build) => {
|
||||
const { optin, optout } = optionalityOf(build())
|
||||
expect(optin).toBeUndefined()
|
||||
@@ -28,12 +28,12 @@ describe('salvaging containers used bare in an object shape', () => {
|
||||
})
|
||||
|
||||
it.each(CONTAINERS)('%s rejects an absent key and an explicit undefined', (_name, build, ok) => {
|
||||
const shape = z.object({ a: build() })
|
||||
const schema = z.object({ a: build() })
|
||||
|
||||
expect(shape.safeParse({}).success).toBe(false)
|
||||
expect(shape.safeParse({ a: undefined }).success).toBe(false)
|
||||
expect(schema.safeParse({}).success).toBe(false)
|
||||
expect(schema.safeParse({ a: undefined }).success).toBe(false)
|
||||
// Why: a positive control, so the two rejections above cannot pass by rejecting everything.
|
||||
expect(shape.safeParse({ a: ok })).toMatchObject({ success: true })
|
||||
expect(schema.safeParse({ a: ok })).toMatchObject({ success: true })
|
||||
})
|
||||
|
||||
it.each(CONTAINERS)(
|
||||
|
||||
@@ -48,7 +48,7 @@ const HOST_TERMINAL_SURFACE_SEPARATOR = '::'
|
||||
/** Daemon session id form. Deliberately excluded from id-shape classification,
|
||||
* which is why a host-created tab needs its own binding to be preserved —
|
||||
* a `serve-`/`ssh-` shaped id would take an already-correct path instead. */
|
||||
function isDaemonShapedPtyId(ptyId: string, worktreeId: string): boolean {
|
||||
function isDaemonPtyIdForm(ptyId: string, worktreeId: string): boolean {
|
||||
return (
|
||||
ptyId.startsWith(`${worktreeId}@@`) &&
|
||||
!ptyId.startsWith('serve-') &&
|
||||
@@ -156,7 +156,7 @@ export async function createHostCliTerminal(
|
||||
throw new Error('Host did not report a leaf id for the CLI-created terminal')
|
||||
}
|
||||
expect(
|
||||
isDaemonShapedPtyId(ptyId, worktreeId),
|
||||
isDaemonPtyIdForm(ptyId, worktreeId),
|
||||
`CLI terminal ${ptyId} must carry the daemon id shape this seam excludes from classification`
|
||||
).toBe(true)
|
||||
await expect
|
||||
|
||||
@@ -73,7 +73,7 @@ const SUBSTITUTION_GROUPS = [
|
||||
* The two ways a substituted keystroke can reach the renderer. Both are real; only the second one
|
||||
* regressed, and only the second one can regress, which is why running both is the point.
|
||||
*/
|
||||
const SUBSTITUTION_SHAPES: readonly {
|
||||
const SUBSTITUTION_ROUTES: readonly {
|
||||
name: string
|
||||
slug: string
|
||||
dispatch: (session: CDPSession, keystroke: SubstitutedKeystroke) => Promise<void>
|
||||
@@ -165,9 +165,9 @@ test.describe('Terminal CJK IME committed text', () => {
|
||||
}
|
||||
})
|
||||
|
||||
for (const shape of SUBSTITUTION_SHAPES) {
|
||||
for (const route of SUBSTITUTION_ROUTES) {
|
||||
for (const group of SUBSTITUTION_GROUPS) {
|
||||
test(`sends full-width ${group.label} and never their ASCII form when ${shape.name}`, async ({
|
||||
test(`sends full-width ${group.label} and never their ASCII form when ${route.name}`, async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
@@ -179,7 +179,7 @@ test.describe('Terminal CJK IME committed text', () => {
|
||||
try {
|
||||
await startTerminalImeByteReader(orcaPage, arena.ptyId, reader)
|
||||
for (const keystroke of group.keystrokes) {
|
||||
await shape.dispatch(arena.session, keystroke)
|
||||
await route.dispatch(arena.session, keystroke)
|
||||
await orcaPage.waitForTimeout(60)
|
||||
}
|
||||
await dispatchPlainEnter(arena.session)
|
||||
@@ -200,7 +200,7 @@ test.describe('Terminal CJK IME committed text', () => {
|
||||
await closeTerminalImePaneArena(
|
||||
arena,
|
||||
testInfo,
|
||||
`full-width-${group.label}-${shape.slug}`,
|
||||
`full-width-${group.label}-${route.slug}`,
|
||||
!completed
|
||||
)
|
||||
removeTerminalImeByteReader(reader)
|
||||
|
||||
@@ -69,7 +69,7 @@ export function parseArgs(argv) {
|
||||
|
||||
function validate(opts, exePathFlagPresent, argv) {
|
||||
const errors = []
|
||||
errors.push(...validateArgShape(argv))
|
||||
errors.push(...validateArgSyntax(argv))
|
||||
if (!opts.expect) {
|
||||
errors.push('Missing --expect <survival|orphaned>')
|
||||
} else if (!VALID_PROFILES.has(opts.expect)) {
|
||||
@@ -94,7 +94,7 @@ function validate(opts, exePathFlagPresent, argv) {
|
||||
return errors
|
||||
}
|
||||
|
||||
function validateArgShape(argv) {
|
||||
function validateArgSyntax(argv) {
|
||||
const errors = []
|
||||
const seen = new Set()
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
|
||||
Reference in New Issue
Block a user