fix(windows): node wrapper updateAoMetadata supports V2 .json metadata format

The Windows Node.js gh/git wrappers in NODE_UPDATE_AO_METADATA only tried
the bare session path (e.g. ao-154), but V2 storage uses ao-154.json files.
This caused silent metadata update failures on Windows — PR URLs written by
agents via `gh pr create` were never recorded in session metadata.

Fix mirrors bash ao-metadata-helper.sh: try .json first (V2), fall back to
bare name (V1/legacy). Also adds JSON.parse/stringify handling for V2 JSON
format instead of the key=value line-splitting that only worked for V1.

Bump WRAPPER_VERSION 0.6.0 → 0.7.0 to force reinstall on existing setups.
This commit is contained in:
copilot-swe-agent[bot] 2026-04-28 23:35:19 +00:00 committed by GitHub
parent 720cbfcb26
commit 6d9c38ae33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 5164 additions and 14 deletions

2466
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

2651
packages/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -97,7 +97,7 @@ describe("setupPathWrapperWorkspace (Unix)", () => {
it("skips wrapper rewrite when version matches", async () => {
mockReadFile
.mockResolvedValueOnce("0.6.0") // version marker matches
.mockResolvedValueOnce("0.7.0") // version marker matches
.mockRejectedValueOnce(new Error("ENOENT")); // AGENTS.md doesn't exist
await setupPathWrapperWorkspace("/workspace");
@ -269,6 +269,27 @@ describe("buildNodeWrapper", () => {
expect(script).toContain("fs.existsSync");
expect(script).toContain("findRealGh()");
});
it("updateAoMetadata in node wrappers handles V2 .json metadata format", () => {
const ghScript = buildNodeWrapper("gh", "");
// Must try the .json extension first (V2 storage format)
expect(ghScript).toContain('aoSession + ".json"');
// Must fall back to bare name (V1 legacy format)
expect(ghScript).toContain("path.join(resolvedDir, aoSession)");
// Must handle JSON.parse/stringify for V2 format
expect(ghScript).toContain("JSON.parse");
expect(ghScript).toContain("JSON.stringify");
// Must handle key=value lines for V1 format
expect(ghScript).toContain('split("\\n")');
});
it("updateAoMetadata is shared between gh and git wrappers", () => {
const ghScript = buildNodeWrapper("gh", "");
const gitScript = buildNodeWrapper("git", "");
// Both wrappers should contain the V2 JSON format support
expect(ghScript).toContain('aoSession + ".json"');
expect(gitScript).toContain('aoSession + ".json"');
});
});
describe("AO_METADATA_HELPER", () => {
@ -400,7 +421,7 @@ describe("GH_WRAPPER", () => {
});
it("uses current wrapper version in trace logging", () => {
expect(GH_WRAPPER).toContain("0.6.0");
expect(GH_WRAPPER).toContain("0.7.0");
});
it("logs cache outcomes (hit/miss-stored/miss-negative/miss-error) to trace", () => {

View File

@ -35,7 +35,7 @@ function getAoBinDir(): string {
}
/** Current version of wrapper scripts — bump when scripts change */
const WRAPPER_VERSION = "0.6.0";
const WRAPPER_VERSION = "0.7.0";
// =============================================================================
// PATH Builder
@ -666,7 +666,11 @@ function updateAoMetadata(key, value) {
const allowed = [path.join(home, ".ao"), path.join(home, ".agent-orchestrator"), os.tmpdir()];
if (!allowed.some(a => resolvedDir === a || resolvedDir.startsWith(a + sep))) return;
const metadataFile = path.join(resolvedDir, aoSession);
// Try V2 (.json) first, then fall back to V1 (bare) — mirrors bash ao-metadata-helper.sh
let metadataFile = path.join(resolvedDir, aoSession + ".json");
if (!fs.existsSync(metadataFile)) {
metadataFile = path.join(resolvedDir, aoSession);
}
if (!fs.existsSync(metadataFile)) return;
// Strip newlines from value
@ -675,18 +679,26 @@ function updateAoMetadata(key, value) {
let content;
try { content = fs.readFileSync(metadataFile, "utf8"); } catch { return; }
const lines = content.split("\\n");
const keyPrefix = key + "=";
const idx = lines.findIndex(l => l.startsWith(keyPrefix));
if (idx >= 0) {
lines[idx] = key + "=" + cleanValue;
} else {
lines.push(key + "=" + cleanValue);
}
const tmpFile = metadataFile + ".tmp." + process.pid;
try {
fs.writeFileSync(tmpFile, lines.join("\\n"), "utf8");
if (metadataFile.endsWith(".json")) {
// V2 JSON format
let d;
try { d = JSON.parse(content); } catch { return; }
d[key] = cleanValue;
fs.writeFileSync(tmpFile, JSON.stringify(d, null, 2), "utf8");
} else {
// V1 key=value format
const lines = content.split("\\n");
const keyPrefix = key + "=";
const idx = lines.findIndex(l => l.startsWith(keyPrefix));
if (idx >= 0) {
lines[idx] = key + "=" + cleanValue;
} else {
lines.push(key + "=" + cleanValue);
}
fs.writeFileSync(tmpFile, lines.join("\\n"), "utf8");
}
fs.renameSync(tmpFile, metadataFile);
} catch {
try { fs.unlinkSync(tmpFile); } catch {}