Skip to content

Commit 52af6c0

Browse files
authored
Merge pull request #154 from Lykhoyda/fix/issue-112-sidecar-windows-path
fix(gh-112): sidecarPathFor handles both POSIX and Windows separators
2 parents 87c9364 + de9653c commit 52af6c0

7 files changed

Lines changed: 96 additions & 5 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{
1010
"name": "rn-dev-agent",
1111
"description": "AI agent that fully tests React Native features on simulator/emulator — navigates the app, verifies UI, walks user flows, and confirms internal state.",
12-
"version": "0.44.40",
12+
"version": "0.44.41",
1313
"source": "./",
1414
"category": "mobile-development",
1515
"homepage": "https://github.com/Lykhoyda/rn-dev-agent"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rn-dev-agent",
3-
"version": "0.44.40",
3+
"version": "0.44.41",
44
"description": "AI agent that fully tests React Native features on simulator/emulator — navigates the app, verifies UI, walks user flows, and confirms internal state.",
55
"author": {
66
"name": "Anton Lykhoyda",

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to rn-dev-agent will be documented in this file.
44

55
Format follows [Keep a Changelog](https://keepachangelog.com/).
66

7+
## [0.44.41] — 2026-05-13
8+
9+
### Fixed (GH #112 — sidecar-io Windows path bug)
10+
11+
- **`sidecarPathFor` now extracts the basename via `split(/[\\/]/).pop()`**
12+
instead of `split('/').pop()`. The old form returned the entire
13+
backslash-containing path as a single segment on Windows, producing
14+
absurd deeply-nested directory trees through subsequent
15+
`join(parent, 'state', base)`. PR #109's atomic-writer trusted this
16+
output and `ensureDir`-ed it, making the pre-existing latent bug more
17+
impactful. Gemini flagged at conf 88 in the PR #109 multi-LLM review.
18+
- 4 new regression tests cover POSIX-style paths, `.yml` extension,
19+
Windows-style backslash input, and mixed-separator input. The fix
20+
works on both POSIX and Windows runtimes since the separator split
21+
is explicit rather than platform-native. Suite: 1312 → 1316 passing.
22+
723
## [0.44.40] — 2026-05-13
824

925
### Hardened (GH #111 — atomic-writer concurrent pairWrite races)

scripts/cdp-bridge/dist/domain/sidecar-io.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@ export function sidecarPathFor(yamlFilePath) {
1212
// <project>/.rn-agent/actions/<id>.yaml → <project>/.rn-agent/state/<id>.state.json
1313
// We don't assume the input is under .rn-agent/actions/ — instead derive
1414
// the sidecar by replacing the YAML's parent dir with sibling `state/`.
15+
//
16+
// GH #112: split on BOTH POSIX and Windows separators. The original
17+
// `split('/').pop()` returned the entire backslash-containing path as a
18+
// single segment on Windows, leading `join(parent, 'state', base)` to
19+
// produce a deeply-nested broken directory tree. Using `path.basename`
20+
// alone isn't enough because on a POSIX runtime `path.basename` doesn't
21+
// recognize `\` as a separator, so a Windows-style input passed through
22+
// unrelated code (e.g. cross-platform test fixtures) would still
23+
// misbehave. Explicit `[/\\]` split is platform-agnostic at the source.
1524
const dir = dirname(yamlFilePath);
1625
const parent = dirname(dir);
17-
const base = yamlFilePath.replace(/\.ya?ml$/i, '.state.json').split('/').pop();
26+
const filename = yamlFilePath.replace(/\.ya?ml$/i, '.state.json');
27+
const base = filename.split(/[\\/]/).pop();
1828
return join(parent, 'state', base);
1929
}
2030
/**

scripts/cdp-bridge/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rn-dev-agent-cdp",
3-
"version": "0.38.35",
3+
"version": "0.38.36",
44
"type": "module",
55
"main": "dist/index.js",
66
"scripts": {

scripts/cdp-bridge/src/domain/sidecar-io.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@ export function sidecarPathFor(yamlFilePath: string): string {
1717
// <project>/.rn-agent/actions/<id>.yaml → <project>/.rn-agent/state/<id>.state.json
1818
// We don't assume the input is under .rn-agent/actions/ — instead derive
1919
// the sidecar by replacing the YAML's parent dir with sibling `state/`.
20+
//
21+
// GH #112: split on BOTH POSIX and Windows separators. The original
22+
// `split('/').pop()` returned the entire backslash-containing path as a
23+
// single segment on Windows, leading `join(parent, 'state', base)` to
24+
// produce a deeply-nested broken directory tree. Using `path.basename`
25+
// alone isn't enough because on a POSIX runtime `path.basename` doesn't
26+
// recognize `\` as a separator, so a Windows-style input passed through
27+
// unrelated code (e.g. cross-platform test fixtures) would still
28+
// misbehave. Explicit `[/\\]` split is platform-agnostic at the source.
2029
const dir = dirname(yamlFilePath);
2130
const parent = dirname(dir);
22-
const base = yamlFilePath.replace(/\.ya?ml$/i, '.state.json').split('/').pop()!;
31+
const filename = yamlFilePath.replace(/\.ya?ml$/i, '.state.json');
32+
const base = filename.split(/[\\/]/).pop()!;
2333
return join(parent, 'state', base);
2434
}
2535

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// GH #112: regression test for sidecarPathFor's basename extraction.
2+
// The original implementation used `split('/').pop()` which returned the
3+
// entire backslash-containing path as a single segment on Windows,
4+
// producing absurd `join(parent, 'state', <full-windows-path>)` deep
5+
// directory trees. The fix splits on both separators explicitly.
6+
//
7+
// We run this on a POSIX runtime (darwin/linux) and assert that the
8+
// EXTRACTED basename is correct for Windows-style input regardless of
9+
// platform-native path module behavior. Parent-dir resolution on a
10+
// Windows path is platform-dependent and out of scope — the goal here is
11+
// only "the basename portion is correctly extracted."
12+
import { test } from 'node:test';
13+
import assert from 'node:assert/strict';
14+
15+
const MOD_PATH = '../../dist/domain/sidecar-io.js';
16+
17+
test('sidecarPathFor: POSIX-style path produces expected sidecar path', async () => {
18+
const { sidecarPathFor } = await import(MOD_PATH);
19+
const result = sidecarPathFor('/Users/me/project/.rn-agent/actions/wizard-create-task.yaml');
20+
assert.equal(result, '/Users/me/project/.rn-agent/state/wizard-create-task.state.json');
21+
});
22+
23+
test('sidecarPathFor: .yml extension also handled', async () => {
24+
const { sidecarPathFor } = await import(MOD_PATH);
25+
const result = sidecarPathFor('/a/b/actions/short.yml');
26+
assert.equal(result, '/a/b/state/short.state.json');
27+
});
28+
29+
test('sidecarPathFor: Windows-style backslash input extracts basename correctly (no embedded path in result)', async () => {
30+
// GH #112: this is the regression case. The buggy code produced a base
31+
// string containing the entire backslash path, which join() then
32+
// embedded into a deeply-nested directory tree. The fix's contract is
33+
// that the result's filename portion is just `<id>.state.json`, not
34+
// the full path. We assert on the END of the result string so we don't
35+
// depend on platform-native dirname/join behavior for Windows inputs
36+
// on a POSIX test runtime.
37+
const { sidecarPathFor } = await import(MOD_PATH);
38+
const result = sidecarPathFor('C:\\Users\\foo\\project\\.rn-agent\\actions\\my-action.yaml');
39+
// The trailing segment must be `my-action.state.json`, not the full
40+
// backslash-containing path.
41+
assert.match(result, /[/\\]my-action\.state\.json$/, `result did not end with clean basename: ${result}`);
42+
// The result must NOT contain the entire Windows-style filename glued in.
43+
assert.ok(!result.includes('C:\\Users\\foo\\project\\.rn-agent\\actions\\my-action.state.json'),
44+
`result still contains the full Windows path: ${result}`);
45+
});
46+
47+
test('sidecarPathFor: mixed forward+backward slash input extracts basename correctly', async () => {
48+
// Defensive: someone hand-builds a path with both separators (e.g.
49+
// pre-processing wasn't normalized). Should still extract the trailing
50+
// segment cleanly.
51+
const { sidecarPathFor } = await import(MOD_PATH);
52+
const result = sidecarPathFor('/a/b\\c/d\\my-action.yaml');
53+
assert.match(result, /[/\\]my-action\.state\.json$/, `mixed-separator result: ${result}`);
54+
assert.ok(!result.includes('a/b\\c/d\\my-action.state.json'));
55+
});

0 commit comments

Comments
 (0)