@neuralnomads/codenomad-dev 0.20.0-dev-20260921-63bbdbb1 → 0.20.0-dev-20260922-e68ac524
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automation/developer-inspection-targets.js +25 -0
- package/dist/automation/developer-inspection-targets.js.map +1 -0
- package/dist/developer-cdp.js +5 -4
- package/dist/developer-cdp.js.map +1 -1
- package/dist/developer-cdp.test.js +18 -0
- package/dist/developer-cdp.test.js.map +1 -1
- package/dist/opencode/session-pruning/navigation-contract.js +1 -0
- package/dist/opencode/session-pruning/navigation-contract.js.map +1 -1
- package/dist/opencode/session-pruning/navigation.test.js +12 -1
- package/dist/opencode/session-pruning/navigation.test.js.map +1 -1
- package/dist/opencode/session-pruning/outline-index.js +14 -1
- package/dist/opencode/session-pruning/outline-index.js.map +1 -1
- package/dist/plugins/session-pruning/plugin.mjs +16 -3
- package/dist/server/__tests__/instance-proxy.test.js +32 -0
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/http-server.js +1 -1
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/routes/automation-plugin.js +28 -15
- package/dist/server/routes/automation-plugin.js.map +1 -1
- package/dist/server/routes/automation-plugin.test.js +10 -6
- package/dist/server/routes/automation-plugin.test.js.map +1 -1
- package/dist/workspaces/__tests__/git-common-directory.test.js +86 -0
- package/dist/workspaces/__tests__/git-common-directory.test.js.map +1 -0
- package/dist/workspaces/__tests__/git-worktree-config.test.js +130 -0
- package/dist/workspaces/__tests__/git-worktree-config.test.js.map +1 -0
- package/dist/workspaces/__tests__/git-worktrees.test.js +49 -1
- package/dist/workspaces/__tests__/git-worktrees.test.js.map +1 -1
- package/dist/workspaces/__tests__/native-worktree-fixture.js +2 -2
- package/dist/workspaces/__tests__/native-worktree-fixture.js.map +1 -1
- package/dist/workspaces/git-common-directory.js +18 -0
- package/dist/workspaces/git-common-directory.js.map +1 -0
- package/dist/workspaces/git-process.js +80 -0
- package/dist/workspaces/git-process.js.map +1 -0
- package/dist/workspaces/git-worktrees.js +64 -10
- package/dist/workspaces/git-worktrees.js.map +1 -1
- package/dist/workspaces/native-worktrees.js +24 -9
- package/dist/workspaces/native-worktrees.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/App-BSkmbNUC.js +51 -0
- package/public/assets/App-BSkmbNUC.js.map +1 -0
- package/public/assets/FilesTab-CznSFRW_.js +3 -0
- package/public/assets/{FilesTab-DOOT4Own.js.map → FilesTab-CznSFRW_.js.map} +1 -1
- package/public/assets/{GitChangesTab-DqS17ZG_.js → GitChangesTab-DLtyzpX0.js} +3 -3
- package/public/assets/{GitChangesTab-DqS17ZG_.js.map → GitChangesTab-DLtyzpX0.js.map} +1 -1
- package/public/assets/{SplitFilePanel-DpXPYY4L.js → SplitFilePanel-BHkyIER4.js} +2 -2
- package/public/assets/{SplitFilePanel-DpXPYY4L.js.map → SplitFilePanel-BHkyIER4.js.map} +1 -1
- package/public/assets/{StatusTab-CynqW2pr.js → StatusTab-BB3L4UEF.js} +2 -2
- package/public/assets/{StatusTab-CynqW2pr.js.map → StatusTab-BB3L4UEF.js.map} +1 -1
- package/public/assets/{align-justify-C_MrQdWv.js → align-justify-BGcr8Y3G.js} +2 -2
- package/public/assets/{align-justify-C_MrQdWv.js.map → align-justify-BGcr8Y3G.js.map} +1 -1
- package/public/assets/{bundle-full-CVPoVh_h.js → bundle-full-CwHbX2Tz.js} +2 -2
- package/public/assets/{bundle-full-CVPoVh_h.js.map → bundle-full-CwHbX2Tz.js.map} +1 -1
- package/public/assets/client-state-CjVIIkWh.js +2 -0
- package/public/assets/client-state-CjVIIkWh.js.map +1 -0
- package/public/assets/{diff-viewer-BzgxNlWD.js → diff-viewer-FNBCogHl.js} +2 -2
- package/public/assets/{diff-viewer-BzgxNlWD.js.map → diff-viewer-FNBCogHl.js.map} +1 -1
- package/public/assets/index-AeE49W2c.js +2 -0
- package/public/assets/index-AeE49W2c.js.map +1 -0
- package/public/assets/index-B875QQLi.js +2 -0
- package/public/assets/index-B875QQLi.js.map +1 -0
- package/public/assets/index-BhInW6hR.js +3 -0
- package/public/assets/index-BhInW6hR.js.map +1 -0
- package/public/assets/index-C2jBtFkI.js +2 -0
- package/public/assets/{index-CrjSY1y2.js.map → index-C2jBtFkI.js.map} +1 -1
- package/public/assets/index-C8zCyB6Z.js +2 -0
- package/public/assets/index-C8zCyB6Z.js.map +1 -0
- package/public/assets/index-C9vJvaFa.js +2 -0
- package/public/assets/index-C9vJvaFa.js.map +1 -0
- package/public/assets/index-CLdQSSfU.js +2 -0
- package/public/assets/index-CLdQSSfU.js.map +1 -0
- package/public/assets/{index-Ce-SrKvP.js → index-CLgpBTF6.js} +2 -2
- package/public/assets/{index-Ce-SrKvP.js.map → index-CLgpBTF6.js.map} +1 -1
- package/public/assets/index-CaYFZAN4.js +2 -0
- package/public/assets/index-CaYFZAN4.js.map +1 -0
- package/public/assets/index-ChudAs0f.js +2 -0
- package/public/assets/index-ChudAs0f.js.map +1 -0
- package/public/assets/index-D292-iMl.js +2 -0
- package/public/assets/index-D292-iMl.js.map +1 -0
- package/public/assets/index-DYBqUC9X.css +1 -0
- package/public/assets/index-Dk3HFhZs.js +2 -0
- package/public/assets/index-Dk3HFhZs.js.map +1 -0
- package/public/assets/{loading-BWjkEyUD.js → loading-D8BcUQaM.js} +2 -2
- package/public/assets/{loading-BWjkEyUD.js.map → loading-D8BcUQaM.js.map} +1 -1
- package/public/assets/main-DRbjgAo8.js +42 -0
- package/public/assets/main-DRbjgAo8.js.map +1 -0
- package/public/assets/{markdown-Bkm0F3oO.js → markdown-CRPEbAqL.js} +4 -4
- package/public/assets/{markdown-Bkm0F3oO.js.map → markdown-CRPEbAqL.js.map} +1 -1
- package/public/assets/{monaco-viewer-C10fFymA.js → monaco-viewer-CG0aPZ-r.js} +2 -2
- package/public/assets/{monaco-viewer-C10fFymA.js.map → monaco-viewer-CG0aPZ-r.js.map} +1 -1
- package/public/assets/{startup-state-settings-card-BTfYhHov.js → startup-state-settings-card-CHqS2Pmg.js} +2 -2
- package/public/assets/{startup-state-settings-card-BTfYhHov.js.map → startup-state-settings-card-CHqS2Pmg.js.map} +1 -1
- package/public/assets/{terminal-square-Dhdgf3hj.js → terminal-square-DhmNJqNG.js} +2 -2
- package/public/assets/{terminal-square-Dhdgf3hj.js.map → terminal-square-DhmNJqNG.js.map} +1 -1
- package/public/assets/{todo-pfT9OVP8.js → todo-DXJ5kP_Z.js} +2 -2
- package/public/assets/{todo-pfT9OVP8.js.map → todo-DXJ5kP_Z.js.map} +1 -1
- package/public/assets/{tool-call-ja_vg-Y6.js → tool-call-Bjo9DalZ.js} +5 -5
- package/public/assets/{tool-call-ja_vg-Y6.js.map → tool-call-Bjo9DalZ.js.map} +1 -1
- package/public/assets/{unified-picker-Bi-fpJBA.js → unified-picker-CxrTM_tY.js} +2 -2
- package/public/assets/{unified-picker-Bi-fpJBA.js.map → unified-picker-CxrTM_tY.js.map} +1 -1
- package/public/assets/{wrap-text-DJHeAvj4.js → wrap-text-BznZeD4J.js} +2 -2
- package/public/assets/{wrap-text-DJHeAvj4.js.map → wrap-text-BznZeD4J.js.map} +1 -1
- package/public/index.html +4 -4
- package/public/loading.html +4 -4
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-bGIUQ-dD.js +0 -49
- package/public/assets/App-bGIUQ-dD.js.map +0 -1
- package/public/assets/FilesTab-DOOT4Own.js +0 -3
- package/public/assets/client-state-DHHjx28k.js +0 -2
- package/public/assets/client-state-DHHjx28k.js.map +0 -1
- package/public/assets/index-B5KIA1QZ.js +0 -2
- package/public/assets/index-B5KIA1QZ.js.map +0 -1
- package/public/assets/index-BO3LYyge.js +0 -2
- package/public/assets/index-BO3LYyge.js.map +0 -1
- package/public/assets/index-Bzngra1L.js +0 -3
- package/public/assets/index-Bzngra1L.js.map +0 -1
- package/public/assets/index-C6-rb6B7.css +0 -1
- package/public/assets/index-CI6ZE9xs.js +0 -2
- package/public/assets/index-CI6ZE9xs.js.map +0 -1
- package/public/assets/index-CSUBS-o9.js +0 -2
- package/public/assets/index-CSUBS-o9.js.map +0 -1
- package/public/assets/index-Cl4PCYg4.js +0 -2
- package/public/assets/index-Cl4PCYg4.js.map +0 -1
- package/public/assets/index-CrjSY1y2.js +0 -2
- package/public/assets/index-D7SuJgSm.js +0 -2
- package/public/assets/index-D7SuJgSm.js.map +0 -1
- package/public/assets/index-Daf72P6o.js +0 -2
- package/public/assets/index-Daf72P6o.js.map +0 -1
- package/public/assets/index-WIaMCSWq.js +0 -2
- package/public/assets/index-WIaMCSWq.js.map +0 -1
- package/public/assets/index-WsSXkxQ_.js +0 -2
- package/public/assets/index-WsSXkxQ_.js.map +0 -1
- package/public/assets/main-Hn7UJb2f.js +0 -42
- package/public/assets/main-Hn7UJb2f.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from "../../opencode/automation-plugin";
|
|
3
|
+
import { DeveloperInspectionTargets } from "../../automation/developer-inspection-targets";
|
|
3
4
|
export function isAutomationPluginRequest(request, deps) {
|
|
4
5
|
if (request.method !== "POST" || request.url.split("?")[0] !== AUTOMATION_BRIDGE_PATH)
|
|
5
6
|
return false;
|
|
@@ -13,6 +14,7 @@ export function isAutomationPluginRequest(request, deps) {
|
|
|
13
14
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
14
15
|
}
|
|
15
16
|
export function registerAutomationPluginRoute(app, deps) {
|
|
17
|
+
const inspectedTargets = new DeveloperInspectionTargets();
|
|
16
18
|
app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {
|
|
17
19
|
if (!isAutomationPluginRequest(request, deps))
|
|
18
20
|
return reply.code(401).send({ error: "Unauthorized automation bridge" });
|
|
@@ -77,37 +79,48 @@ export function registerAutomationPluginRoute(app, deps) {
|
|
|
77
79
|
endpoint: status.cdpUrl,
|
|
78
80
|
runId: status.runId,
|
|
79
81
|
windowId: status.windowId,
|
|
80
|
-
sessionId: body.sessionID,
|
|
81
82
|
};
|
|
82
|
-
let
|
|
83
|
+
let command;
|
|
84
|
+
if (body.mode !== "developer-probe") {
|
|
85
|
+
try {
|
|
86
|
+
command = parseDeveloperAction(body.command);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let identity;
|
|
83
93
|
try {
|
|
84
|
-
|
|
94
|
+
if (body.mode === "developer-probe" || command?.action === "inspect") {
|
|
95
|
+
await deps.developerCdp.context(selection);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
identity = inspectedTargets.get(body.sessionID, selection);
|
|
99
|
+
await deps.developerCdp.context(identity);
|
|
100
|
+
}
|
|
85
101
|
}
|
|
86
102
|
catch (error) {
|
|
103
|
+
if (body.mode !== "developer-probe")
|
|
104
|
+
inspectedTargets.forget(body.sessionID);
|
|
87
105
|
return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
return reply.code(404).send({ error: "The visible CodeNomad workspace does not own this OpenCode session" });
|
|
91
|
-
}
|
|
92
|
-
const identity = { ...selection, instanceId: context.instanceId };
|
|
107
|
+
identity ??= selection;
|
|
93
108
|
if (body.mode === "developer-probe") {
|
|
94
109
|
return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } });
|
|
95
110
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
command = parseDeveloperAction(body.command);
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
102
|
-
}
|
|
111
|
+
if (!command)
|
|
112
|
+
return reply.code(400).send({ error: "Missing developer command" });
|
|
103
113
|
try {
|
|
104
114
|
if (command.action === "restart") {
|
|
115
|
+
inspectedTargets.forget(body.sessionID);
|
|
105
116
|
const result = await deps.nativeParent.request("developer.restart", {});
|
|
106
117
|
deps.developerCdp.close(status.runId);
|
|
107
118
|
return reply.send({ result });
|
|
108
119
|
}
|
|
109
120
|
if (command.action === "inspect") {
|
|
110
|
-
|
|
121
|
+
const result = await deps.developerCdp.inspect(identity);
|
|
122
|
+
inspectedTargets.remember(body.sessionID, identity);
|
|
123
|
+
return reply.send({ result });
|
|
111
124
|
}
|
|
112
125
|
if (command.action === "screenshot") {
|
|
113
126
|
const image = await deps.developerCdp.screenshot(identity);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-plugin.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { timingSafeEqual } from \"node:crypto\"\nimport type { FastifyInstance, FastifyRequest } from \"fastify\"\nimport type { AuthManager } from \"../../auth/manager\"\nimport type { DeveloperCdp } from \"../../developer-cdp\"\nimport type { DeveloperCdpIdentity, DeveloperCdpSelection } from \"../../developer-cdp\"\nimport type { NativeParent } from \"../../native-parent\"\nimport { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from \"../../opencode/automation-plugin\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\n\ninterface AutomationPluginRouteDeps {\n authManager: AuthManager\n bridgeToken: string\n nativeParent: NativeParent\n workspaceManager: WorkspaceManager\n developerCdp: DeveloperCdp\n}\n\ninterface DeveloperNativeStatus {\n status: {\n state: string\n runId?: string\n nativeIdentity?: string\n cdpUrl?: string\n windowId?: string\n }\n logs?: unknown[]\n}\n\nexport function isAutomationPluginRequest(\n request: FastifyRequest,\n deps: Pick<AutomationPluginRouteDeps, \"authManager\" | \"bridgeToken\">,\n): boolean {\n if (request.method !== \"POST\" || request.url.split(\"?\")[0] !== AUTOMATION_BRIDGE_PATH) return false\n if (!deps.authManager.isLoopbackRequest(request)) return false\n const supplied = request.headers[\"x-codenomad-automation-token\"]\n if (typeof supplied !== \"string\") return false\n const actual = Buffer.from(supplied)\n const expected = Buffer.from(deps.bridgeToken)\n return actual.length === expected.length && timingSafeEqual(actual, expected)\n}\n\nexport function registerAutomationPluginRoute(app: FastifyInstance, deps: AutomationPluginRouteDeps): void {\n app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {\n if (!isAutomationPluginRequest(request, deps)) return reply.code(401).send({ error: \"Unauthorized automation bridge\" })\n const body = request.body as { mode?: unknown; sessionID?: unknown; command?: unknown } | undefined\n if (!body || ![\"developer-probe\", \"developer-execute\", \"browser-claim\", \"browser-probe\", \"browser-execute\"].includes(String(body.mode))\n || typeof body.sessionID !== \"string\" || body.sessionID.length > 256) {\n return reply.code(400).send({ error: \"Invalid automation bridge request\" })\n }\n\n let location\n try {\n location = (await (await deps.workspaceManager.getSharedServiceClient()).session.get({ sessionID: body.sessionID })).location\n } catch {\n return reply.code(404).send({ error: \"Session not found\" })\n }\n const owned = (await Promise.all(deps.workspaceManager.list().map((workspace) =>\n deps.workspaceManager.ownsLocation(workspace.id, location).catch(() => false),\n ))).some(Boolean)\n if (!owned) return reply.code(404).send({ error: \"Session is not owned by this CodeNomad instance\" })\n\n if (body.mode === \"browser-claim\") return reply.send({ result: { available: true } })\n if (body.mode === \"browser-probe\") {\n try {\n const result = await deps.nativeParent.request<{ available: boolean }>(\"browser.probe\", { sessionID: body.sessionID })\n return result.available ? reply.send({ result }) : reply.code(404).send({ error: \"No visible browser target\" })\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n if (body.mode === \"browser-execute\") {\n let command\n try {\n command = parseBrowserAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n const result = await deps.nativeParent.request(\"browser.execute\", { sessionID: body.sessionID, command })\n return reply.send({ result })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n let native: DeveloperNativeStatus\n try {\n native = await deps.nativeParent.request<DeveloperNativeStatus>(\"developer.status\", {})\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n const status = native.status\n const available = status?.state === \"ready\" && typeof status.runId === \"string\"\n && typeof status.nativeIdentity === \"string\" && typeof status.cdpUrl === \"string\" && typeof status.windowId === \"string\"\n if (!available) {\n if (typeof status?.runId === \"string\") deps.developerCdp.close(status.runId)\n return reply.code(404).send({ error: \"Native automation has no active CodeNomad session\" })\n }\n\n const selection: DeveloperCdpSelection = {\n endpoint: status.cdpUrl!,\n runId: status.runId!,\n windowId: status.windowId!,\n sessionId: body.sessionID,\n }\n let context\n try {\n context = await deps.developerCdp.context(selection)\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n if (!await deps.workspaceManager.ownsLocation(context.instanceId, location)) {\n return reply.code(404).send({ error: \"The visible CodeNomad workspace does not own this OpenCode session\" })\n }\n const identity: DeveloperCdpIdentity = { ...selection, instanceId: context.instanceId }\n if (body.mode === \"developer-probe\") {\n return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } })\n }\n\n let command\n try {\n command = parseDeveloperAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n if (command.action === \"restart\") {\n const result = await deps.nativeParent.request<DeveloperNativeStatus[\"status\"]>(\"developer.restart\", {})\n deps.developerCdp.close(status.runId)\n return reply.send({ result })\n }\n if (command.action === \"inspect\") {\n return reply.send({ result: await deps.developerCdp.inspect(identity) })\n }\n if (command.action === \"screenshot\") {\n const image = await deps.developerCdp.screenshot(identity)\n return reply.send({ result: { image: { data: image.data, mime: image.mediaType } } })\n }\n await deps.developerCdp.act(command.action === \"type\"\n ? { ...identity, kind: \"type\", ref: command.ref, text: command.text }\n : { ...identity, kind: \"click\", ref: command.ref })\n return reply.send({ result: { ok: true } })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"automation-plugin.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { timingSafeEqual } from \"node:crypto\"\nimport type { FastifyInstance, FastifyRequest } from \"fastify\"\nimport type { AuthManager } from \"../../auth/manager\"\nimport type { DeveloperCdp } from \"../../developer-cdp\"\nimport type { DeveloperCdpIdentity, DeveloperCdpSelection } from \"../../developer-cdp\"\nimport type { NativeParent } from \"../../native-parent\"\nimport { AUTOMATION_BRIDGE_PATH, parseBrowserAction, parseDeveloperAction } from \"../../opencode/automation-plugin\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport { DeveloperInspectionTargets } from \"../../automation/developer-inspection-targets\"\n\ninterface AutomationPluginRouteDeps {\n authManager: AuthManager\n bridgeToken: string\n nativeParent: NativeParent\n workspaceManager: WorkspaceManager\n developerCdp: DeveloperCdp\n}\n\ninterface DeveloperNativeStatus {\n status: {\n state: string\n runId?: string\n nativeIdentity?: string\n cdpUrl?: string\n windowId?: string\n }\n logs?: unknown[]\n}\n\nexport function isAutomationPluginRequest(\n request: FastifyRequest,\n deps: Pick<AutomationPluginRouteDeps, \"authManager\" | \"bridgeToken\">,\n): boolean {\n if (request.method !== \"POST\" || request.url.split(\"?\")[0] !== AUTOMATION_BRIDGE_PATH) return false\n if (!deps.authManager.isLoopbackRequest(request)) return false\n const supplied = request.headers[\"x-codenomad-automation-token\"]\n if (typeof supplied !== \"string\") return false\n const actual = Buffer.from(supplied)\n const expected = Buffer.from(deps.bridgeToken)\n return actual.length === expected.length && timingSafeEqual(actual, expected)\n}\n\nexport function registerAutomationPluginRoute(app: FastifyInstance, deps: AutomationPluginRouteDeps): void {\n const inspectedTargets = new DeveloperInspectionTargets()\n app.post(AUTOMATION_BRIDGE_PATH, { bodyLimit: 32 * 1024 }, async (request, reply) => {\n if (!isAutomationPluginRequest(request, deps)) return reply.code(401).send({ error: \"Unauthorized automation bridge\" })\n const body = request.body as { mode?: unknown; sessionID?: unknown; command?: unknown } | undefined\n if (!body || ![\"developer-probe\", \"developer-execute\", \"browser-claim\", \"browser-probe\", \"browser-execute\"].includes(String(body.mode))\n || typeof body.sessionID !== \"string\" || body.sessionID.length > 256) {\n return reply.code(400).send({ error: \"Invalid automation bridge request\" })\n }\n\n let location\n try {\n location = (await (await deps.workspaceManager.getSharedServiceClient()).session.get({ sessionID: body.sessionID })).location\n } catch {\n return reply.code(404).send({ error: \"Session not found\" })\n }\n const owned = (await Promise.all(deps.workspaceManager.list().map((workspace) =>\n deps.workspaceManager.ownsLocation(workspace.id, location).catch(() => false),\n ))).some(Boolean)\n if (!owned) return reply.code(404).send({ error: \"Session is not owned by this CodeNomad instance\" })\n\n if (body.mode === \"browser-claim\") return reply.send({ result: { available: true } })\n if (body.mode === \"browser-probe\") {\n try {\n const result = await deps.nativeParent.request<{ available: boolean }>(\"browser.probe\", { sessionID: body.sessionID })\n return result.available ? reply.send({ result }) : reply.code(404).send({ error: \"No visible browser target\" })\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n if (body.mode === \"browser-execute\") {\n let command\n try {\n command = parseBrowserAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n try {\n const result = await deps.nativeParent.request(\"browser.execute\", { sessionID: body.sessionID, command })\n return reply.send({ result })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n let native: DeveloperNativeStatus\n try {\n native = await deps.nativeParent.request<DeveloperNativeStatus>(\"developer.status\", {})\n } catch (error) {\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n const status = native.status\n const available = status?.state === \"ready\" && typeof status.runId === \"string\"\n && typeof status.nativeIdentity === \"string\" && typeof status.cdpUrl === \"string\" && typeof status.windowId === \"string\"\n if (!available) {\n if (typeof status?.runId === \"string\") deps.developerCdp.close(status.runId)\n return reply.code(404).send({ error: \"Native automation has no active CodeNomad session\" })\n }\n\n const selection: DeveloperCdpSelection = {\n endpoint: status.cdpUrl!,\n runId: status.runId!,\n windowId: status.windowId!,\n }\n let command\n if (body.mode !== \"developer-probe\") {\n try {\n command = parseDeveloperAction(body.command)\n } catch (error) {\n return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })\n }\n }\n let identity: DeveloperCdpIdentity | undefined\n try {\n if (body.mode === \"developer-probe\" || command?.action === \"inspect\") {\n await deps.developerCdp.context(selection)\n } else {\n identity = inspectedTargets.get(body.sessionID, selection)\n await deps.developerCdp.context(identity)\n }\n } catch (error) {\n if (body.mode !== \"developer-probe\") inspectedTargets.forget(body.sessionID)\n return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) })\n }\n identity ??= selection\n if (body.mode === \"developer-probe\") {\n return reply.send({ result: { available: true, nativeIdentity: status.nativeIdentity, runId: status.runId } })\n }\n\n if (!command) return reply.code(400).send({ error: \"Missing developer command\" })\n try {\n if (command.action === \"restart\") {\n inspectedTargets.forget(body.sessionID)\n const result = await deps.nativeParent.request<DeveloperNativeStatus[\"status\"]>(\"developer.restart\", {})\n deps.developerCdp.close(status.runId)\n return reply.send({ result })\n }\n if (command.action === \"inspect\") {\n const result = await deps.developerCdp.inspect(identity)\n inspectedTargets.remember(body.sessionID, identity)\n return reply.send({ result })\n }\n if (command.action === \"screenshot\") {\n const image = await deps.developerCdp.screenshot(identity)\n return reply.send({ result: { image: { data: image.data, mime: image.mediaType } } })\n }\n await deps.developerCdp.act(command.action === \"type\"\n ? { ...identity, kind: \"type\", ref: command.ref, text: command.text }\n : { ...identity, kind: \"click\", ref: command.ref })\n return reply.send({ result: { ok: true } })\n } catch (error) {\n return reply.code(409).send({ error: error instanceof Error ? error.message : String(error) })\n }\n })\n}\n"]}
|
|
@@ -3,7 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import Fastify from "fastify";
|
|
4
4
|
import { AUTOMATION_BRIDGE_PATH } from "../../opencode/automation-plugin";
|
|
5
5
|
import { registerAutomationPluginRoute } from "./automation-plugin";
|
|
6
|
-
test("
|
|
6
|
+
test("targets the inspected window independently of the visible conversation", async () => {
|
|
7
7
|
const app = Fastify({ logger: false });
|
|
8
8
|
const nativeCalls = [];
|
|
9
9
|
let state = "ready";
|
|
@@ -34,8 +34,7 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
|
|
|
34
34
|
},
|
|
35
35
|
developerCdp: {
|
|
36
36
|
context: async (identity) => {
|
|
37
|
-
|
|
38
|
-
throw new Error("active session mismatch");
|
|
37
|
+
assert.equal(identity.sessionId, undefined);
|
|
39
38
|
return { windowId: "window-1", instanceId: "workspace-1", sessionId: visibleSession };
|
|
40
39
|
},
|
|
41
40
|
inspect: async (identity) => {
|
|
@@ -48,6 +47,10 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
|
|
|
48
47
|
};
|
|
49
48
|
},
|
|
50
49
|
close: () => undefined,
|
|
50
|
+
act: async (identity) => {
|
|
51
|
+
assert.equal(identity.sessionId, undefined);
|
|
52
|
+
assert.equal(identity.instanceId, undefined);
|
|
53
|
+
},
|
|
51
54
|
},
|
|
52
55
|
workspaceManager: {
|
|
53
56
|
getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: "D:\\project" } }) } }),
|
|
@@ -68,17 +71,18 @@ test("fences Developer Mode by the visible owned session and forwards CDP action
|
|
|
68
71
|
endpoint: "http://127.0.0.1:9222",
|
|
69
72
|
runId: "run-1",
|
|
70
73
|
windowId: "window-1",
|
|
71
|
-
sessionId: "session-1",
|
|
72
|
-
instanceId: "workspace-1",
|
|
73
74
|
});
|
|
74
75
|
assert.equal(inspect.json().result.context.sessionId, "session-1");
|
|
76
|
+
visibleSession = "session-2";
|
|
77
|
+
assert.equal((await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "click", ref: "ax1" } })).statusCode, 200);
|
|
75
78
|
const restart = await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "restart" } });
|
|
76
79
|
assert.equal(restart.statusCode, 200);
|
|
77
80
|
assert.deepEqual(nativeCalls.slice(-2), [
|
|
78
81
|
{ method: "developer.status", params: {} },
|
|
79
82
|
{ method: "developer.restart", params: {} },
|
|
80
83
|
]);
|
|
81
|
-
assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode,
|
|
84
|
+
assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode, 200);
|
|
85
|
+
assert.equal((await request({ mode: "developer-execute", sessionID: "session-1", command: { action: "restart" } })).statusCode, 404);
|
|
82
86
|
state = "stopped";
|
|
83
87
|
assert.equal((await request({ mode: "developer-probe", sessionID: "session-2" })).statusCode, 404);
|
|
84
88
|
state = "ready";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport test from \"node:test\"\nimport Fastify from \"fastify\"\nimport { AUTOMATION_BRIDGE_PATH } from \"../../opencode/automation-plugin\"\nimport { registerAutomationPluginRoute } from \"./automation-plugin\"\n\ntest(\"fences Developer Mode by the visible owned session and forwards CDP actions\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n let state = \"ready\"\n let runId = \"run-1\"\n let visibleSession = \"session-1\"\n let inspectedIdentity: unknown\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n if (method === \"developer.restart\") {\n runId = \"run-2\"\n return { state: \"starting\", runId }\n }\n return {\n status: {\n state,\n runId,\n nativeIdentity: \"electron:test\",\n cdpUrl: \"http://127.0.0.1:9222\",\n windowId: \"window-1\",\n },\n logs: [],\n }\n },\n },\n developerCdp: {\n context: async (identity: { sessionId: string }) => {\n if (identity.sessionId !== visibleSession) throw new Error(\"active session mismatch\")\n return { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession }\n },\n inspect: async (identity: unknown) => {\n inspectedIdentity = identity\n return {\n target: { id: \"page-1\", title: \"CodeNomad\", url: \"http://app.test/\" },\n context: { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession },\n nodes: [],\n diagnostics: [],\n }\n },\n close: () => undefined,\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"workspace-1\" }],\n ownsLocation: async () => true,\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const inspect = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(inspect.statusCode, 200)\n assert.deepEqual(inspectedIdentity, {\n endpoint: \"http://127.0.0.1:9222\",\n runId: \"run-1\",\n windowId: \"window-1\",\n sessionId: \"session-1\",\n instanceId: \"workspace-1\",\n })\n assert.equal(inspect.json().result.context.sessionId, \"session-1\")\n\n const restart = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })\n assert.equal(restart.statusCode, 200)\n assert.deepEqual(nativeCalls.slice(-2), [\n { method: \"developer.status\", params: {} },\n { method: \"developer.restart\", params: {} },\n ])\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n\n state = \"stopped\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n state = \"ready\"\n runId = \"run-3\"\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n\n const unauthorized = await app.inject({ method: \"POST\", url: AUTOMATION_BRIDGE_PATH, payload: { mode: \"developer-probe\", sessionID: \"session-1\" } })\n assert.equal(unauthorized.statusCode, 401)\n await app.close()\n})\n\ntest(\"browser works with an unavailable developer target and unrelated inventory failure\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n return method === \"developer.status\" ? { status: { state: \"stopped\" } }\n : method === \"browser.probe\" ? { available: true } : { url: \"https://example.com/\" }\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"unrelated-worktree\" }, { id: \"workspace-1\" }],\n ownsLocation: async (id: string) => {\n if (id === \"unrelated-worktree\") throw new Error(\"Native worktree inventory is unavailable\")\n return true\n },\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"browser-claim\", sessionID: \"session-1\" })).statusCode, 200)\n assert.equal(nativeCalls.length, 0)\n assert.equal((await request({ mode: \"browser-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const open = await request({ mode: \"browser-execute\", sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } })\n assert.equal(open.statusCode, 200)\n assert.deepEqual(nativeCalls, [\n { method: \"browser.probe\", params: { sessionID: \"session-1\" } },\n {\n method: \"browser.execute\",\n params: { sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } },\n },\n ])\n const developer = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(developer.statusCode, 404)\n assert.match(developer.json().error, /Native automation has no active/)\n await app.close()\n})\n"]}
|
|
1
|
+
{"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../../src/server/routes/automation-plugin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport test from \"node:test\"\nimport Fastify from \"fastify\"\nimport { AUTOMATION_BRIDGE_PATH } from \"../../opencode/automation-plugin\"\nimport { registerAutomationPluginRoute } from \"./automation-plugin\"\n\ntest(\"targets the inspected window independently of the visible conversation\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n let state = \"ready\"\n let runId = \"run-1\"\n let visibleSession = \"session-1\"\n let inspectedIdentity: unknown\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n if (method === \"developer.restart\") {\n runId = \"run-2\"\n return { state: \"starting\", runId }\n }\n return {\n status: {\n state,\n runId,\n nativeIdentity: \"electron:test\",\n cdpUrl: \"http://127.0.0.1:9222\",\n windowId: \"window-1\",\n },\n logs: [],\n }\n },\n },\n developerCdp: {\n context: async (identity: { sessionId?: string }) => {\n assert.equal(identity.sessionId, undefined)\n return { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession }\n },\n inspect: async (identity: unknown) => {\n inspectedIdentity = identity\n return {\n target: { id: \"page-1\", title: \"CodeNomad\", url: \"http://app.test/\" },\n context: { windowId: \"window-1\", instanceId: \"workspace-1\", sessionId: visibleSession },\n nodes: [],\n diagnostics: [],\n }\n },\n close: () => undefined,\n act: async (identity: { sessionId?: string; instanceId?: string }) => {\n assert.equal(identity.sessionId, undefined)\n assert.equal(identity.instanceId, undefined)\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"workspace-1\" }],\n ownsLocation: async () => true,\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const inspect = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(inspect.statusCode, 200)\n assert.deepEqual(inspectedIdentity, {\n endpoint: \"http://127.0.0.1:9222\",\n runId: \"run-1\",\n windowId: \"window-1\",\n })\n assert.equal(inspect.json().result.context.sessionId, \"session-1\")\n\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"click\", ref: \"ax1\" } })).statusCode, 200)\n\n const restart = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })\n assert.equal(restart.statusCode, 200)\n assert.deepEqual(nativeCalls.slice(-2), [\n { method: \"developer.status\", params: {} },\n { method: \"developer.restart\", params: {} },\n ])\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n assert.equal((await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"restart\" } })).statusCode, 404)\n\n state = \"stopped\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 404)\n state = \"ready\"\n runId = \"run-3\"\n visibleSession = \"session-2\"\n assert.equal((await request({ mode: \"developer-probe\", sessionID: \"session-2\" })).statusCode, 200)\n\n const unauthorized = await app.inject({ method: \"POST\", url: AUTOMATION_BRIDGE_PATH, payload: { mode: \"developer-probe\", sessionID: \"session-1\" } })\n assert.equal(unauthorized.statusCode, 401)\n await app.close()\n})\n\ntest(\"browser works with an unavailable developer target and unrelated inventory failure\", async () => {\n const app = Fastify({ logger: false })\n const nativeCalls: Array<{ method: string; params: unknown }> = []\n registerAutomationPluginRoute(app, {\n authManager: { isLoopbackRequest: () => true },\n bridgeToken: \"secret\",\n nativeParent: {\n request: async (method: string, params: unknown) => {\n nativeCalls.push({ method, params })\n return method === \"developer.status\" ? { status: { state: \"stopped\" } }\n : method === \"browser.probe\" ? { available: true } : { url: \"https://example.com/\" }\n },\n },\n workspaceManager: {\n getSharedServiceClient: async () => ({ session: { get: async () => ({ location: { directory: \"D:\\\\project\" } }) } }),\n list: () => [{ id: \"unrelated-worktree\" }, { id: \"workspace-1\" }],\n ownsLocation: async (id: string) => {\n if (id === \"unrelated-worktree\") throw new Error(\"Native worktree inventory is unavailable\")\n return true\n },\n },\n } as never)\n\n const request = async (body: Record<string, unknown>) => app.inject({\n method: \"POST\",\n url: AUTOMATION_BRIDGE_PATH,\n headers: { \"x-codenomad-automation-token\": \"secret\" },\n payload: body,\n })\n\n assert.equal((await request({ mode: \"browser-claim\", sessionID: \"session-1\" })).statusCode, 200)\n assert.equal(nativeCalls.length, 0)\n assert.equal((await request({ mode: \"browser-probe\", sessionID: \"session-1\" })).statusCode, 200)\n const open = await request({ mode: \"browser-execute\", sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } })\n assert.equal(open.statusCode, 200)\n assert.deepEqual(nativeCalls, [\n { method: \"browser.probe\", params: { sessionID: \"session-1\" } },\n {\n method: \"browser.execute\",\n params: { sessionID: \"session-1\", command: { action: \"open\", url: \"https://example.com\" } },\n },\n ])\n const developer = await request({ mode: \"developer-execute\", sessionID: \"session-1\", command: { action: \"inspect\" } })\n assert.equal(developer.statusCode, 404)\n assert.match(developer.json().error, /Native automation has no active/)\n await app.close()\n})\n"]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { readGitCommonDirectory } from "../git-common-directory";
|
|
8
|
+
import { sharesGitCommonDirectory } from "../git-worktrees";
|
|
9
|
+
const git = (directory, ...args) => execFileSync("git", ["-C", directory, ...args], {
|
|
10
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
|
|
11
|
+
}).trim();
|
|
12
|
+
describe("Git ownership preflight admission", () => {
|
|
13
|
+
let temp;
|
|
14
|
+
let repo;
|
|
15
|
+
let linked;
|
|
16
|
+
let trace;
|
|
17
|
+
let environment;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_")));
|
|
20
|
+
for (const key of Object.keys(environment))
|
|
21
|
+
delete process.env[key];
|
|
22
|
+
temp = mkdtempSync(path.join(tmpdir(), "codenomad-git-preflight-"));
|
|
23
|
+
process.env.GIT_CONFIG_NOSYSTEM = "1";
|
|
24
|
+
process.env.GIT_CONFIG_GLOBAL = path.join(temp, "empty-global-config");
|
|
25
|
+
writeFileSync(process.env.GIT_CONFIG_GLOBAL, "");
|
|
26
|
+
repo = path.join(temp, "main repo");
|
|
27
|
+
linked = path.join(temp, "linked checkout");
|
|
28
|
+
git(temp, "init", "--initial-branch=main", repo);
|
|
29
|
+
git(repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
|
|
30
|
+
git(repo, "worktree", "add", "-b", "feature", linked);
|
|
31
|
+
trace = path.join(temp, "trace.json");
|
|
32
|
+
process.env.GIT_TRACE2_EVENT = trace.replaceAll("\\", "/");
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
for (const key of Object.keys(process.env))
|
|
36
|
+
if (key.startsWith("GIT_"))
|
|
37
|
+
delete process.env[key];
|
|
38
|
+
Object.assign(process.env, environment);
|
|
39
|
+
rmSync(temp, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
const events = (file) => readFileSync(file, "utf8").trim().split("\n").map(line => JSON.parse(line));
|
|
42
|
+
it("coalesces a concurrent session/project ownership fan into one command per directory", async () => {
|
|
43
|
+
const reads = Array.from({ length: 48 }, () => sharesGitCommonDirectory(repo, linked));
|
|
44
|
+
assert.deepEqual(await Promise.all(reads), Array(48).fill(true));
|
|
45
|
+
const starts = events(trace).filter(event => event.event === "start");
|
|
46
|
+
assert.equal(starts.length, 2);
|
|
47
|
+
assert.deepEqual(starts.map(event => event.argv[2]).sort(), [repo, linked].sort());
|
|
48
|
+
});
|
|
49
|
+
it("bounds distinct Git processes without holding the calling event loop", async () => {
|
|
50
|
+
const directories = Array.from({ length: 10 }, (_, index) => path.join(temp, `repo-${index}`));
|
|
51
|
+
for (const directory of directories)
|
|
52
|
+
git(temp, "init", directory);
|
|
53
|
+
writeFileSync(trace, "");
|
|
54
|
+
const reads = directories.map(readGitCommonDirectory);
|
|
55
|
+
let settled = false;
|
|
56
|
+
void Promise.all(reads).then(() => { settled = true; });
|
|
57
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
58
|
+
assert.equal(settled, false);
|
|
59
|
+
assert.deepEqual(await Promise.all(reads), directories.map(directory => realpathSync(path.join(directory, ".git"))));
|
|
60
|
+
const timeline = events(trace).filter(event => event.event === "start" || event.event === "exit")
|
|
61
|
+
.sort((a, b) => a.time.localeCompare(b.time));
|
|
62
|
+
let running = 0;
|
|
63
|
+
let starts = 0;
|
|
64
|
+
for (const event of timeline) {
|
|
65
|
+
running += event.event === "start" ? 1 : -1;
|
|
66
|
+
if (event.event === "start")
|
|
67
|
+
starts += 1;
|
|
68
|
+
assert.ok(running >= 0 && running <= 2, `concurrent Git processes: ${running}`);
|
|
69
|
+
}
|
|
70
|
+
assert.equal(starts, directories.length);
|
|
71
|
+
assert.equal(running, 0);
|
|
72
|
+
});
|
|
73
|
+
it("rechecks changed Git identity and does not retain failed reads", async () => {
|
|
74
|
+
assert.equal(await sharesGitCommonDirectory(repo, linked), true);
|
|
75
|
+
const other = path.join(temp, "independent clone");
|
|
76
|
+
git(temp, "clone", repo, other);
|
|
77
|
+
rmSync(path.join(linked, ".git"));
|
|
78
|
+
writeFileSync(path.join(linked, ".git"), `gitdir: ${path.join(other, ".git").replaceAll("\\", "/")}\n`);
|
|
79
|
+
assert.equal(await sharesGitCommonDirectory(repo, linked), false);
|
|
80
|
+
const missing = path.join(temp, "not-yet-created");
|
|
81
|
+
await assert.rejects(readGitCommonDirectory(missing));
|
|
82
|
+
git(temp, "init", missing);
|
|
83
|
+
assert.equal(await readGitCommonDirectory(missing), realpathSync(path.join(missing, ".git")));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=git-common-directory.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-common-directory.test.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/git-common-directory.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, beforeEach, describe, it } from \"node:test\"\nimport { readGitCommonDirectory } from \"../git-common-directory\"\nimport { sharesGitCommonDirectory } from \"../git-worktrees\"\n\nconst git = (directory: string, ...args: string[]) => execFileSync(\"git\", [\"-C\", directory, ...args], {\n encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"], windowsHide: true,\n}).trim()\n\ndescribe(\"Git ownership preflight admission\", () => {\n let temp: string\n let repo: string\n let linked: string\n let trace: string\n let environment: Record<string, string | undefined>\n\n beforeEach(() => {\n environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith(\"GIT_\")))\n for (const key of Object.keys(environment)) delete process.env[key]\n temp = mkdtempSync(path.join(tmpdir(), \"codenomad-git-preflight-\"))\n process.env.GIT_CONFIG_NOSYSTEM = \"1\"\n process.env.GIT_CONFIG_GLOBAL = path.join(temp, \"empty-global-config\")\n writeFileSync(process.env.GIT_CONFIG_GLOBAL, \"\")\n repo = path.join(temp, \"main repo\")\n linked = path.join(temp, \"linked checkout\")\n git(temp, \"init\", \"--initial-branch=main\", repo)\n git(repo, \"-c\", \"user.name=Test\", \"-c\", \"user.email=test@example.com\", \"commit\", \"--allow-empty\", \"-m\", \"initial\")\n git(repo, \"worktree\", \"add\", \"-b\", \"feature\", linked)\n trace = path.join(temp, \"trace.json\")\n process.env.GIT_TRACE2_EVENT = trace.replaceAll(\"\\\\\", \"/\")\n })\n\n afterEach(() => {\n for (const key of Object.keys(process.env)) if (key.startsWith(\"GIT_\")) delete process.env[key]\n Object.assign(process.env, environment)\n rmSync(temp, { recursive: true, force: true })\n })\n\n const events = (file: string) => readFileSync(file, \"utf8\").trim().split(\"\\n\").map(line => JSON.parse(line))\n\n it(\"coalesces a concurrent session/project ownership fan into one command per directory\", async () => {\n const reads = Array.from({ length: 48 }, () => sharesGitCommonDirectory(repo, linked))\n assert.deepEqual(await Promise.all(reads), Array(48).fill(true))\n const starts = events(trace).filter(event => event.event === \"start\")\n assert.equal(starts.length, 2)\n assert.deepEqual(starts.map(event => event.argv[2]).sort(), [repo, linked].sort())\n })\n\n it(\"bounds distinct Git processes without holding the calling event loop\", async () => {\n const directories = Array.from({ length: 10 }, (_, index) => path.join(temp, `repo-${index}`))\n for (const directory of directories) git(temp, \"init\", directory)\n writeFileSync(trace, \"\")\n const reads = directories.map(readGitCommonDirectory)\n let settled = false\n void Promise.all(reads).then(() => { settled = true })\n await new Promise<void>(resolve => setImmediate(resolve))\n assert.equal(settled, false)\n assert.deepEqual(await Promise.all(reads), directories.map(directory => realpathSync(path.join(directory, \".git\"))))\n const timeline = events(trace).filter(event => event.event === \"start\" || event.event === \"exit\")\n .sort((a, b) => a.time.localeCompare(b.time))\n let running = 0\n let starts = 0\n for (const event of timeline) {\n running += event.event === \"start\" ? 1 : -1\n if (event.event === \"start\") starts += 1\n assert.ok(running >= 0 && running <= 2, `concurrent Git processes: ${running}`)\n }\n assert.equal(starts, directories.length)\n assert.equal(running, 0)\n })\n\n it(\"rechecks changed Git identity and does not retain failed reads\", async () => {\n assert.equal(await sharesGitCommonDirectory(repo, linked), true)\n const other = path.join(temp, \"independent clone\")\n git(temp, \"clone\", repo, other)\n rmSync(path.join(linked, \".git\"))\n writeFileSync(path.join(linked, \".git\"), `gitdir: ${path.join(other, \".git\").replaceAll(\"\\\\\", \"/\")}\\n`)\n assert.equal(await sharesGitCommonDirectory(repo, linked), false)\n const missing = path.join(temp, \"not-yet-created\")\n await assert.rejects(readGitCommonDirectory(missing))\n git(temp, \"init\", missing)\n assert.equal(await readGitCommonDirectory(missing), realpathSync(path.join(missing, \".git\")))\n })\n})\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { fixtureCatalogue } from "./native-worktree-fixture";
|
|
8
|
+
const git = (directory, ...args) => execFileSync("git", ["-C", directory, ...args], {
|
|
9
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
}).trim();
|
|
11
|
+
const gitPath = (directory) => directory.replaceAll("\\", "/");
|
|
12
|
+
describe("configured Git checkout roots", () => {
|
|
13
|
+
let temp;
|
|
14
|
+
let repo;
|
|
15
|
+
let linked;
|
|
16
|
+
let redirected;
|
|
17
|
+
let environment;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_")));
|
|
20
|
+
for (const key of Object.keys(environment))
|
|
21
|
+
delete process.env[key];
|
|
22
|
+
temp = mkdtempSync(path.join(tmpdir(), "codenomad-checkout-config-"));
|
|
23
|
+
process.env.GIT_CONFIG_NOSYSTEM = "1";
|
|
24
|
+
process.env.GIT_CONFIG_GLOBAL = path.join(temp, "empty-global-config");
|
|
25
|
+
writeFileSync(process.env.GIT_CONFIG_GLOBAL, "");
|
|
26
|
+
repo = path.join(temp, "main repo");
|
|
27
|
+
linked = path.join(temp, "linked checkout");
|
|
28
|
+
redirected = path.join(temp, "effective root");
|
|
29
|
+
git(temp, "init", "--initial-branch=main", repo);
|
|
30
|
+
git(repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
|
|
31
|
+
git(repo, "worktree", "add", "-b", "feature", linked);
|
|
32
|
+
mkdirSync(redirected);
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
for (const key of Object.keys(process.env))
|
|
36
|
+
if (key.startsWith("GIT_"))
|
|
37
|
+
delete process.env[key];
|
|
38
|
+
Object.assign(process.env, environment);
|
|
39
|
+
rmSync(temp, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
for (const mode of ["absolute", "relative"]) {
|
|
42
|
+
it(`rejects an administrative backlink redirected by ${mode} configuration`, async () => {
|
|
43
|
+
mkdirSync(path.join(repo, "nested"));
|
|
44
|
+
mkdirSync(path.join(linked, "nested"));
|
|
45
|
+
const workspace = path.join(repo, "nested");
|
|
46
|
+
const before = await fixtureCatalogue(workspace);
|
|
47
|
+
assert.equal(before.worktrees.length, 2);
|
|
48
|
+
assert.equal(before.worktrees.find(entry => entry.kind === "worktree")?.directory, path.join(realpathSync(linked), "nested"));
|
|
49
|
+
const admin = git(linked, "rev-parse", "--absolute-git-dir");
|
|
50
|
+
const backlink = readFileSync(path.join(admin, "gitdir"), "utf8");
|
|
51
|
+
git(repo, "config", "extensions.worktreeConfig", "true");
|
|
52
|
+
git(linked, "config", "--worktree", "core.worktree", mode === "absolute" ? redirected : gitPath(path.relative(admin, redirected)));
|
|
53
|
+
assert.equal(realpathSync(git(linked, "rev-parse", "--show-toplevel")), realpathSync(redirected));
|
|
54
|
+
assert.equal(readFileSync(path.join(admin, "gitdir"), "utf8"), backlink);
|
|
55
|
+
await assert.rejects(fixtureCatalogue(workspace), /not a checkout root/);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
it("rejects a linked checkout made bare through worktree config", async () => {
|
|
59
|
+
git(repo, "config", "extensions.worktreeConfig", "true");
|
|
60
|
+
git(linked, "config", "--worktree", "core.bare", "true");
|
|
61
|
+
assert.equal(git(linked, "rev-parse", "--is-bare-repository"), "true");
|
|
62
|
+
await assert.rejects(fixtureCatalogue(repo), /must be run in a work tree/);
|
|
63
|
+
});
|
|
64
|
+
it("checks a redirected main checkout when opened from an ordinary linked checkout", async () => {
|
|
65
|
+
git(repo, "config", "core.worktree", redirected);
|
|
66
|
+
assert.equal(realpathSync(git(repo, "rev-parse", "--show-toplevel")), realpathSync(redirected));
|
|
67
|
+
assert.equal(realpathSync(git(linked, "rev-parse", "--show-toplevel")), realpathSync(linked));
|
|
68
|
+
await assert.rejects(fixtureCatalogue(linked), /not a checkout root/);
|
|
69
|
+
});
|
|
70
|
+
for (const mode of ["worktree", "conditional shared", "conditional global"]) {
|
|
71
|
+
it(`uses Git's effective root semantics for ${mode} includes`, async () => {
|
|
72
|
+
const admin = git(linked, "rev-parse", "--absolute-git-dir");
|
|
73
|
+
const included = gitPath(path.join(temp, "root-config"));
|
|
74
|
+
writeFileSync(included, `[core]\n\tworktree = "${gitPath(redirected)}"\n`);
|
|
75
|
+
if (mode === "worktree") {
|
|
76
|
+
git(repo, "config", "extensions.worktreeConfig", "true");
|
|
77
|
+
git(linked, "config", "--worktree", "include.path", included);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
git(repo, "config", mode === "conditional global" ? "--global" : "--local", `includeIf.gitdir:${gitPath(admin)}.path`, included);
|
|
81
|
+
// The main checkout does not activate this condition.
|
|
82
|
+
assert.throws(() => git(repo, "config", "--get", "core.worktree"));
|
|
83
|
+
}
|
|
84
|
+
assert.equal(git(linked, "config", "--includes", "--get", "core.worktree"), gitPath(redirected));
|
|
85
|
+
// Git's early repository setup can ignore included core.worktree even
|
|
86
|
+
// though `config --get` reports it. Do not emulate that setup in JS.
|
|
87
|
+
const effectiveRoot = realpathSync(git(linked, "rev-parse", "--show-toplevel"));
|
|
88
|
+
if (effectiveRoot === realpathSync(linked)) {
|
|
89
|
+
assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await assert.rejects(fixtureCatalogue(repo), /not a checkout root/);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
it("accepts unredirected worktree configuration and still rejects substituted backlinks", async () => {
|
|
97
|
+
git(repo, "config", "extensions.worktreeConfig", "true");
|
|
98
|
+
git(linked, "config", "--worktree", "core.worktree", linked);
|
|
99
|
+
assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2);
|
|
100
|
+
const other = path.join(temp, "other checkout");
|
|
101
|
+
git(repo, "worktree", "add", "--detach", other);
|
|
102
|
+
rmSync(path.join(linked, ".git"));
|
|
103
|
+
writeFileSync(path.join(linked, ".git"), readFileSync(path.join(other, ".git")));
|
|
104
|
+
await assert.rejects(fixtureCatalogue(repo), /not a checkout root/);
|
|
105
|
+
});
|
|
106
|
+
it("keeps the ordinary scan process count constant with benign includes and more checkouts", async () => {
|
|
107
|
+
const included = path.join(temp, "benign-config");
|
|
108
|
+
writeFileSync(included, "[core]\n\tbare = false\n[user]\n\tname = Test\n");
|
|
109
|
+
git(repo, "config", "include.path", gitPath(included));
|
|
110
|
+
const other = path.join(temp, "detached checkout");
|
|
111
|
+
git(repo, "worktree", "add", "--detach", other);
|
|
112
|
+
const trace = path.join(temp, "git-trace.json");
|
|
113
|
+
process.env.GIT_TRACE2_EVENT = gitPath(trace);
|
|
114
|
+
const scan = async (directories) => {
|
|
115
|
+
writeFileSync(trace, "");
|
|
116
|
+
const result = await fixtureCatalogue(repo, directories);
|
|
117
|
+
const starts = readFileSync(trace, "utf8").trim().split("\n").map(line => JSON.parse(line))
|
|
118
|
+
.filter(event => event.event === "start" && event.argv.some((arg) => arg.startsWith(temp)));
|
|
119
|
+
assert.equal(starts.filter(event => event.argv.includes("--porcelain")).length, 1);
|
|
120
|
+
assert.equal(starts.filter(event => event.argv.includes("--includes")).length, 1);
|
|
121
|
+
return { result, commands: starts.map(event => event.argv.slice(3)) };
|
|
122
|
+
};
|
|
123
|
+
const one = await scan([repo]);
|
|
124
|
+
const many = await scan([repo, linked, other]);
|
|
125
|
+
assert.equal(many.result.worktrees.length, 3);
|
|
126
|
+
assert.deepEqual(many.commands.sort(), one.commands.sort());
|
|
127
|
+
assert.equal(many.result.worktrees.find(entry => entry.branch === "feature")?.removable, true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=git-worktree-config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-worktree-config.test.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/git-worktree-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, beforeEach, describe, it } from \"node:test\"\nimport { fixtureCatalogue } from \"./native-worktree-fixture\"\n\nconst git = (directory: string, ...args: string[]) => execFileSync(\"git\", [\"-C\", directory, ...args], {\n encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"],\n}).trim()\nconst gitPath = (directory: string) => directory.replaceAll(\"\\\\\", \"/\")\n\ndescribe(\"configured Git checkout roots\", () => {\n let temp: string\n let repo: string\n let linked: string\n let redirected: string\n let environment: Record<string, string | undefined>\n\n beforeEach(() => {\n environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith(\"GIT_\")))\n for (const key of Object.keys(environment)) delete process.env[key]\n temp = mkdtempSync(path.join(tmpdir(), \"codenomad-checkout-config-\"))\n process.env.GIT_CONFIG_NOSYSTEM = \"1\"\n process.env.GIT_CONFIG_GLOBAL = path.join(temp, \"empty-global-config\")\n writeFileSync(process.env.GIT_CONFIG_GLOBAL, \"\")\n repo = path.join(temp, \"main repo\")\n linked = path.join(temp, \"linked checkout\")\n redirected = path.join(temp, \"effective root\")\n git(temp, \"init\", \"--initial-branch=main\", repo)\n git(repo, \"-c\", \"user.name=Test\", \"-c\", \"user.email=test@example.com\", \"commit\", \"--allow-empty\", \"-m\", \"initial\")\n git(repo, \"worktree\", \"add\", \"-b\", \"feature\", linked)\n mkdirSync(redirected)\n })\n\n afterEach(() => {\n for (const key of Object.keys(process.env)) if (key.startsWith(\"GIT_\")) delete process.env[key]\n Object.assign(process.env, environment)\n rmSync(temp, { recursive: true, force: true })\n })\n\n for (const mode of [\"absolute\", \"relative\"] as const) {\n it(`rejects an administrative backlink redirected by ${mode} configuration`, async () => {\n mkdirSync(path.join(repo, \"nested\"))\n mkdirSync(path.join(linked, \"nested\"))\n const workspace = path.join(repo, \"nested\")\n const before = await fixtureCatalogue(workspace)\n assert.equal(before.worktrees.length, 2)\n assert.equal(before.worktrees.find(entry => entry.kind === \"worktree\")?.directory, path.join(realpathSync(linked), \"nested\"))\n const admin = git(linked, \"rev-parse\", \"--absolute-git-dir\")\n const backlink = readFileSync(path.join(admin, \"gitdir\"), \"utf8\")\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.worktree\",\n mode === \"absolute\" ? redirected : gitPath(path.relative(admin, redirected)))\n assert.equal(realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\")), realpathSync(redirected))\n assert.equal(readFileSync(path.join(admin, \"gitdir\"), \"utf8\"), backlink)\n await assert.rejects(fixtureCatalogue(workspace), /not a checkout root/)\n })\n }\n\n it(\"rejects a linked checkout made bare through worktree config\", async () => {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.bare\", \"true\")\n assert.equal(git(linked, \"rev-parse\", \"--is-bare-repository\"), \"true\")\n await assert.rejects(fixtureCatalogue(repo), /must be run in a work tree/)\n })\n\n it(\"checks a redirected main checkout when opened from an ordinary linked checkout\", async () => {\n git(repo, \"config\", \"core.worktree\", redirected)\n assert.equal(realpathSync(git(repo, \"rev-parse\", \"--show-toplevel\")), realpathSync(redirected))\n assert.equal(realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\")), realpathSync(linked))\n await assert.rejects(fixtureCatalogue(linked), /not a checkout root/)\n })\n\n for (const mode of [\"worktree\", \"conditional shared\", \"conditional global\"] as const) {\n it(`uses Git's effective root semantics for ${mode} includes`, async () => {\n const admin = git(linked, \"rev-parse\", \"--absolute-git-dir\")\n const included = gitPath(path.join(temp, \"root-config\"))\n writeFileSync(included, `[core]\\n\\tworktree = \"${gitPath(redirected)}\"\\n`)\n if (mode === \"worktree\") {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"include.path\", included)\n } else {\n git(repo, \"config\", mode === \"conditional global\" ? \"--global\" : \"--local\",\n `includeIf.gitdir:${gitPath(admin)}.path`, included)\n // The main checkout does not activate this condition.\n assert.throws(() => git(repo, \"config\", \"--get\", \"core.worktree\"))\n }\n assert.equal(git(linked, \"config\", \"--includes\", \"--get\", \"core.worktree\"), gitPath(redirected))\n // Git's early repository setup can ignore included core.worktree even\n // though `config --get` reports it. Do not emulate that setup in JS.\n const effectiveRoot = realpathSync(git(linked, \"rev-parse\", \"--show-toplevel\"))\n if (effectiveRoot === realpathSync(linked)) {\n assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2)\n } else {\n await assert.rejects(fixtureCatalogue(repo), /not a checkout root/)\n }\n })\n }\n\n it(\"accepts unredirected worktree configuration and still rejects substituted backlinks\", async () => {\n git(repo, \"config\", \"extensions.worktreeConfig\", \"true\")\n git(linked, \"config\", \"--worktree\", \"core.worktree\", linked)\n assert.equal((await fixtureCatalogue(repo)).worktrees.length, 2)\n const other = path.join(temp, \"other checkout\")\n git(repo, \"worktree\", \"add\", \"--detach\", other)\n rmSync(path.join(linked, \".git\"))\n writeFileSync(path.join(linked, \".git\"), readFileSync(path.join(other, \".git\")))\n await assert.rejects(fixtureCatalogue(repo), /not a checkout root/)\n })\n\n it(\"keeps the ordinary scan process count constant with benign includes and more checkouts\", async () => {\n const included = path.join(temp, \"benign-config\")\n writeFileSync(included, \"[core]\\n\\tbare = false\\n[user]\\n\\tname = Test\\n\")\n git(repo, \"config\", \"include.path\", gitPath(included))\n const other = path.join(temp, \"detached checkout\")\n git(repo, \"worktree\", \"add\", \"--detach\", other)\n const trace = path.join(temp, \"git-trace.json\")\n process.env.GIT_TRACE2_EVENT = gitPath(trace)\n const scan = async (directories: string[]) => {\n writeFileSync(trace, \"\")\n const result = await fixtureCatalogue(repo, directories)\n const starts = readFileSync(trace, \"utf8\").trim().split(\"\\n\").map(line => JSON.parse(line))\n .filter(event => event.event === \"start\" && event.argv.some((arg: string) => arg.startsWith(temp)))\n assert.equal(starts.filter(event => event.argv.includes(\"--porcelain\")).length, 1)\n assert.equal(starts.filter(event => event.argv.includes(\"--includes\")).length, 1)\n return { result, commands: starts.map(event => event.argv.slice(3)) }\n }\n const one = await scan([repo])\n const many = await scan([repo, linked, other])\n assert.equal(many.result.worktrees.length, 3)\n assert.deepEqual(many.commands.sort(), one.commands.sort())\n assert.equal(many.result.worktrees.find(entry => entry.branch === \"feature\")?.removable, true)\n })\n})\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { describe, it } from "node:test";
|
|
@@ -27,5 +27,53 @@ describe("listWorktrees", () => {
|
|
|
27
27
|
rmSync(temp, { recursive: true, force: true });
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
+
it("batches named and detached annotations while retaining native membership and physical repository ownership", async () => {
|
|
31
|
+
const temp = mkdtempSync(path.join(tmpdir(), "codenomad-worktree-annotations-"));
|
|
32
|
+
const repo = path.join(temp, "main repo");
|
|
33
|
+
const named = path.join(temp, "named checkout");
|
|
34
|
+
const detached = path.join(temp, "detached checkout");
|
|
35
|
+
const gitOnly = path.join(temp, "not in native inventory");
|
|
36
|
+
const foreign = path.join(temp, "independent clone");
|
|
37
|
+
const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim();
|
|
38
|
+
try {
|
|
39
|
+
git("init", "--initial-branch=main", repo);
|
|
40
|
+
git("-C", repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "initial");
|
|
41
|
+
const head = git("-C", repo, "rev-parse", "HEAD");
|
|
42
|
+
git("-C", repo, "worktree", "add", "-b", "feature/named", named);
|
|
43
|
+
git("-C", repo, "worktree", "add", "--detach", detached);
|
|
44
|
+
git("-C", repo, "worktree", "add", "--detach", gitOnly);
|
|
45
|
+
git("clone", "--local", repo, foreign);
|
|
46
|
+
const directories = [repo, named, detached, foreign];
|
|
47
|
+
const catalogue = await fixtureCatalogue(repo, directories);
|
|
48
|
+
assert.equal(catalogue.worktrees.length, 3);
|
|
49
|
+
const entry = catalogue.worktrees.find(item => item.directory === realpathSync(named));
|
|
50
|
+
assert.equal(entry.branch, "feature/named");
|
|
51
|
+
assert.equal(entry.head, head);
|
|
52
|
+
assert.equal(entry.removable, true);
|
|
53
|
+
const detachedEntry = catalogue.worktrees.find(item => item.directory === realpathSync(detached));
|
|
54
|
+
assert.equal(detachedEntry.branch, undefined);
|
|
55
|
+
assert.equal(detachedEntry.head, head);
|
|
56
|
+
assert.match(detachedEntry.label, /detached checkout @ /);
|
|
57
|
+
assert.equal(catalogue.worktrees[0].removable, false);
|
|
58
|
+
const pointer = readFileSync(path.join(named, ".git"), "utf8");
|
|
59
|
+
try {
|
|
60
|
+
// A registered path can be replaced. Its current Git identity must still
|
|
61
|
+
// belong to this repository; a stale registration alone is insufficient.
|
|
62
|
+
rmSync(path.join(named, ".git"));
|
|
63
|
+
writeFileSync(path.join(named, ".git"), `gitdir: ${path.join(foreign, ".git")}\n`);
|
|
64
|
+
const replaced = await fixtureCatalogue(repo, directories);
|
|
65
|
+
assert.equal(replaced.worktrees.some(item => item.directory === realpathSync(named)), false);
|
|
66
|
+
writeFileSync(path.join(named, ".git"), readFileSync(path.join(detached, ".git")));
|
|
67
|
+
await assert.rejects(fixtureCatalogue(repo, directories), /not a checkout root/);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rmSync(path.join(named, ".git"), { force: true });
|
|
71
|
+
writeFileSync(path.join(named, ".git"), pointer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rmSync(temp, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
30
78
|
});
|
|
31
79
|
//# sourceMappingURL=git-worktrees.test.js.map
|