Merge feature_extract_toggle_decision_libjs into vibes for human review

Preserves the individual refactor commit as required by the agent git workflow.

Written by AI agent working for @jtmorris. Model: grok-build-0.1.
This commit is contained in:
Vybe (Coding Agent) 2026-05-23 04:41:48 +00:00
commit b124a3e6e3
3 changed files with 43 additions and 21 deletions

View file

@ -16,7 +16,6 @@ PluginComponent {
property var peers: []
property string _copyText: ""
property int _copyIndex: 0
property bool _pendingToggle: false // transient one-shot for post-action verification (to be removed in first-principles refactor)
layerNamespacePlugin: "tailscalectl"
popoutWidth: 360
@ -34,11 +33,11 @@ PluginComponent {
onExited: (code, status) => {
if (code !== 0) {
var action = root.isConnected ? "disconnect" : "connect"
var detail = toggleProcess.stderr.text.trim()
ToastService.showError("tailscalectl", TailscaleLib.formatError(action, detail))
var action = root.isConnected ? "disconnect" : "connect";
var detail = toggleProcess.stderr.text.trim();
ToastService.showError("tailscalectl", TailscaleLib.formatError(action, detail));
}
statusCheck.running = true
root.refreshStatus();
}
}
@ -66,10 +65,10 @@ PluginComponent {
onExited: (code, status) => {
if (code !== 0) {
var detail = exitNodeProcess.stderr.text.trim()
ToastService.showError("tailscalectl", TailscaleLib.formatError("set", detail))
var detail = exitNodeProcess.stderr.text.trim();
ToastService.showError("tailscalectl", TailscaleLib.formatError("set", detail));
}
statusCheck.running = true
root.refreshStatus();
}
}
@ -95,19 +94,22 @@ PluginComponent {
ToastService.showError("tailscalectl", TailscaleLib.formatError("status"))
}
// Post-action verification: if a toggle was requested, use the fresh state we just received.
if (root._pendingToggle) {
root._pendingToggle = false
toggleProcess.command = TailscaleLib.buildToggleCommand(root.isConnected)
toggleProcess.running = true
const cmd = TailscaleLib.commandForPendingAction(statusCheck._pendingAction, root.isConnected);
if (cmd) {
toggleProcess.command = cmd;
toggleProcess.running = true;
}
statusCheck._pendingAction = null;
}
}
function toggleTailscale() {
// Post-action verification: force a fresh status check, then act on the real ground truth.
root._pendingToggle = true
statusCheck.running = true
statusCheck._pendingAction = "toggle";
statusCheck.running = true;
}
function refreshStatus() {
statusCheck.running = true;
}
function setExitNode(hostname) {

View file

@ -125,7 +125,17 @@ function formatError(action, detail) {
return base;
}
// CommonJS export for Node.js tests (ignored by QML)
if (typeof module !== "undefined" && module.exports) {
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, formatError, getStatusCommand, isValidExitNodeHostname, getClipboardCommands, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode }
const PendingAction = Object.freeze({
TOGGLE: "toggle"
});
function commandForPendingAction(pending, freshIsConnected) {
if (pending === PendingAction.TOGGLE) {
return buildToggleCommand(freshIsConnected);
}
return null;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, formatError, getStatusCommand, isValidExitNodeHostname, getClipboardCommands, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode, PendingAction, commandForPendingAction };
}

View file

@ -1,7 +1,7 @@
import { test } from "node:test"
import assert from "node:assert"
import lib from "../tailscalectl/lib.js"
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, formatError, getStatusCommand, isValidExitNodeHostname, getClipboardCommands, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode } = lib
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, formatError, getStatusCommand, isValidExitNodeHostname, getClipboardCommands, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode, PendingAction, commandForPendingAction } = lib
/*
* Unit tests for the pure functions exported from lib.js.
@ -151,7 +151,17 @@ test("buildToggleCommand treats null and undefined as disconnected", () => {
assert.deepStrictEqual(buildToggleCommand(undefined), ["tailscale", "up"])
})
// --- parseStatusResult ---
test("commandForPendingAction returns toggle command when pending is TOGGLE and passes through buildToggleCommand logic", () => {
assert.deepStrictEqual(commandForPendingAction(PendingAction.TOGGLE, true), ["tailscale", "down"]);
assert.deepStrictEqual(commandForPendingAction(PendingAction.TOGGLE, false), ["tailscale", "up"]);
assert.deepStrictEqual(commandForPendingAction(PendingAction.TOGGLE, null), ["tailscale", "up"]);
});
test("commandForPendingAction returns null for no pending action or unknown pending value", () => {
assert.strictEqual(commandForPendingAction(null, true), null);
assert.strictEqual(commandForPendingAction(undefined, false), null);
assert.strictEqual(commandForPendingAction("something-else", true), null);
});
test("parseStatusResult produces correct state from valid JSON", () => {
const json = JSON.stringify({