agent-yes 1.287.0 → 1.288.0
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/{SUPPORTED_CLIS-DKdFELZL.js → SUPPORTED_CLIS-BHowMgMY.js} +2 -2
- package/dist/{SUPPORTED_CLIS-5YPYQ8Iw.js → SUPPORTED_CLIS-ChhJyq4h.js} +3 -3
- package/dist/{agentShare-B525ly6v.js → agentShare-T3RskvDT.js} +2 -2
- package/dist/{callback-C-awM4qn.js → callback-CuohbCJA.js} +3 -3
- package/dist/{callback-BLTW4Mgp.js → callback-DeaETuHt.js} +2 -2
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-BRtmnpsM.js → cmdPs-Bm4JG7hi.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DshQvO-4.js → notifyDaemon-BORqPeAl.js} +2 -2
- package/dist/{parentPingLoop-Bjw6hKoZ.js → parentPingLoop-COePRYr9.js} +2 -2
- package/dist/{rustBinary-C57KWEtX.js → rustBinary-BmXzqj5z.js} +2 -2
- package/dist/{rustBinary-BXhp8yBp.js → rustBinary-D12k7iOD.js} +2 -2
- package/dist/{schedule-Boa0nE0V.js → schedule-DLoUmyJh.js} +4 -4
- package/dist/{serve-7mcHebep.js → serve-DaDlOtss.js} +17 -17
- package/dist/{setup-DXRL-Mdo.js → setup-Cqy8nRrq.js} +2 -2
- package/dist/{subcommands-BftthBK7.js → subcommands-BfiOyfz7.js} +15 -15
- package/dist/{subcommands-BQJ-Uqtl.js → subcommands-DKTa3E6O.js} +1 -1
- package/dist/{terminal-DrYf3HP0.js → terminal-C0tnGnrZ.js} +2 -2
- package/dist/trayApp-BrAMlCY7.js +5 -0
- package/dist/{trayApp-DG2fQnhx.js → trayApp-DvL9qW6V.js} +2 -2
- package/dist/{ts-LDnoqCV2.js → ts-DJLp2_bv.js} +3 -3
- package/dist/{versionChecker-CGmyt2qE.js → versionChecker-CkO7wYyV.js} +2 -2
- package/dist/{widget-B2SluyG1.js → widget-CJqpScCG.js} +3 -3
- package/dist/{ws-DWA90-uE.js → ws-DGENGNrr.js} +2 -2
- package/dist/{ws-CTOnVXg8.js → ws-DzCKvzWD.js} +2 -2
- package/lab/ui/index.html +95 -67
- package/package.json +2 -1
- package/ts/JsonlStore.bun.spec.ts +204 -0
- package/ts/ReadyManager.bun.spec.ts +72 -0
- package/ts/agentNice.bun.spec.ts +24 -0
- package/ts/agentPermissions.bun.spec.ts +117 -0
- package/ts/agentShare.bun.spec.ts +240 -0
- package/ts/agentTree.bun.spec.ts +92 -0
- package/ts/agentYesHome.bun.spec.ts +25 -0
- package/ts/askApi.bun.spec.ts +358 -0
- package/ts/askCli.bun.spec.ts +304 -0
- package/ts/autoRetry.bun.spec.ts +123 -0
- package/ts/badges.bun.spec.ts +187 -0
- package/ts/buildRustArgs.bun.spec.ts +351 -0
- package/ts/callbackCore.bun.spec.ts +139 -0
- package/ts/channels/hlc.bun.spec.ts +56 -0
- package/ts/channels/link.bun.spec.ts +72 -0
- package/ts/channels/op.bun.spec.ts +78 -0
- package/ts/channels/store.browser.bun.spec.ts +60 -0
- package/ts/channels/store.bun.spec.ts +145 -0
- package/ts/channels/store.node.bun.spec.ts +76 -0
- package/ts/channels/trust.bun.spec.ts +86 -0
- package/ts/channels.bun.spec.ts +212 -0
- package/ts/cli-idle.bun.spec.ts +20 -0
- package/ts/cmdDsh.bun.spec.ts +90 -0
- package/ts/configLoader.bun.spec.ts +273 -0
- package/ts/cwdConflictWarn.bun.spec.ts +60 -0
- package/ts/cwdPassthroughHint.bun.spec.ts +74 -0
- package/ts/e2e-crypto.bun.spec.ts +235 -0
- package/ts/forkNested.bun.spec.ts +240 -0
- package/ts/histStore.bun.spec.ts +355 -0
- package/ts/identity.bun.spec.ts +190 -0
- package/ts/idleWaiter.bun.spec.ts +91 -0
- package/ts/initMsg.bun.spec.ts +132 -0
- package/ts/invokedCli.bun.spec.ts +30 -0
- package/ts/ipcLock.bun.spec.ts +107 -0
- package/ts/logger.bun.spec.ts +27 -0
- package/ts/lsWatch.bun.spec.ts +61 -0
- package/ts/messageLog.bun.spec.ts +145 -0
- package/ts/needsInput.bun.spec.ts +157 -0
- package/ts/nodeRuntime.bun.spec.ts +99 -0
- package/ts/notifyDaemon.bun.spec.ts +297 -0
- package/ts/notifyInbox.bun.spec.ts +277 -0
- package/ts/notifyRouter.bun.spec.ts +351 -0
- package/ts/notifyStore.bun.spec.ts +403 -0
- package/ts/ownerHeartbeat.bun.spec.ts +175 -0
- package/ts/parentLink.bun.spec.ts +64 -0
- package/ts/parentPing.bun.spec.ts +176 -0
- package/ts/parentPingLoop.bun.spec.ts +79 -0
- package/ts/parentPingSend.bun.spec.ts +195 -0
- package/ts/parentWatching.bun.spec.ts +61 -0
- package/ts/pidStore.bun.spec.ts +372 -0
- package/ts/procStartTime.bun.spec.ts +117 -0
- package/ts/reaper.bun.spec.ts +86 -0
- package/ts/removeControlCharacters.bun.spec.ts +94 -0
- package/ts/resolveBinary.bun.spec.ts +133 -0
- package/ts/resultEnvelope.bun.spec.ts +46 -0
- package/ts/runningLock.bun.spec.ts +485 -0
- package/ts/rustBinary.bun.spec.ts +81 -0
- package/ts/schedule.bun.spec.ts +30 -0
- package/ts/serve.bun.spec.ts +166 -0
- package/ts/serveLock.bun.spec.ts +220 -0
- package/ts/session-integration.bun.spec.ts +93 -0
- package/ts/setup.bun.spec.ts +42 -0
- package/ts/share.bun.spec.ts +36 -0
- package/ts/sizeNego.bun.spec.ts +56 -0
- package/ts/statusText.bun.spec.ts +17 -0
- package/ts/strayServe.bun.spec.ts +50 -0
- package/ts/subcommandMirror.bun.spec.ts +76 -0
- package/ts/systemPathLink.bun.spec.ts +79 -0
- package/ts/termToken.bun.spec.ts +74 -0
- package/ts/terminal.bun.spec.ts +50 -0
- package/ts/terminalReply.bun.spec.ts +36 -0
- package/ts/titleScanner.bun.spec.ts +65 -0
- package/ts/todoAutomation.bun.spec.ts +253 -0
- package/ts/todoBlock.bun.spec.ts +73 -0
- package/ts/todoDigest.bun.spec.ts +184 -0
- package/ts/todoIdentity.bun.spec.ts +85 -0
- package/ts/todoLifecycle.bun.spec.ts +79 -0
- package/ts/todoParse.bun.spec.ts +98 -0
- package/ts/todoRoot.bun.spec.ts +117 -0
- package/ts/todoStore.bun.spec.ts +727 -0
- package/ts/tryCatch.bun.spec.ts +275 -0
- package/ts/webrtcLink.bun.spec.ts +45 -0
- package/ts/workspaceConfig.bun.spec.ts +319 -0
- package/dist/trayApp-1AQ6X2V9.js +0 -5
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
AUTO_RETRY_MAX_DELAY_SECS,
|
|
4
|
+
AUTO_RETRY_REASON_FALLBACK,
|
|
5
|
+
AUTO_RETRY_REASONS,
|
|
6
|
+
autoRetryBackoffMs,
|
|
7
|
+
buildAutoRetryMessage,
|
|
8
|
+
classifyAutoRetryReason,
|
|
9
|
+
formatDurationSecs,
|
|
10
|
+
shouldFireRetry,
|
|
11
|
+
} from "./autoRetry.ts";
|
|
12
|
+
import { loadSharedCliDefaults } from "./configShared.ts";
|
|
13
|
+
|
|
14
|
+
describe("autoRetryBackoffMs", () => {
|
|
15
|
+
it("doubles 8,16,32,…,256 then caps", () => {
|
|
16
|
+
expect(autoRetryBackoffMs(0)).toBe(8_000);
|
|
17
|
+
expect(autoRetryBackoffMs(1)).toBe(16_000);
|
|
18
|
+
expect(autoRetryBackoffMs(2)).toBe(32_000);
|
|
19
|
+
expect(autoRetryBackoffMs(3)).toBe(64_000);
|
|
20
|
+
expect(autoRetryBackoffMs(4)).toBe(128_000);
|
|
21
|
+
expect(autoRetryBackoffMs(5)).toBe(256_000);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("caps at the max delay and never overflows for large streaks", () => {
|
|
25
|
+
expect(autoRetryBackoffMs(6)).toBe(AUTO_RETRY_MAX_DELAY_SECS * 1000);
|
|
26
|
+
expect(autoRetryBackoffMs(50)).toBe(AUTO_RETRY_MAX_DELAY_SECS * 1000);
|
|
27
|
+
expect(Number.isFinite(autoRetryBackoffMs(1000))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("shouldFireRetry", () => {
|
|
32
|
+
it("requires ready + not working + past the idle window", () => {
|
|
33
|
+
// Busy — never fire even if otherwise ready and quiet.
|
|
34
|
+
expect(shouldFireRetry(true, true, 10_000, 5_000)).toBe(false);
|
|
35
|
+
// Not at a recognized ready prompt — don't fire.
|
|
36
|
+
expect(shouldFireRetry(false, false, 10_000, 5_000)).toBe(false);
|
|
37
|
+
// Ready and idle, but the quiet window hasn't elapsed yet (user may still
|
|
38
|
+
// be mid-typing) — defer.
|
|
39
|
+
expect(shouldFireRetry(false, true, 4_999, 5_000)).toBe(false);
|
|
40
|
+
// Ready, idle, and past the quiet window — fire.
|
|
41
|
+
expect(shouldFireRetry(false, true, 5_000, 5_000)).toBe(true);
|
|
42
|
+
expect(shouldFireRetry(false, true, 10_000, 5_000)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("classifyAutoRetryReason", () => {
|
|
47
|
+
it("maps real banners to inert paraphrases", () => {
|
|
48
|
+
expect(classifyAutoRetryReason("● API Error: 529 Overloaded")).toBe(AUTO_RETRY_REASONS[0]);
|
|
49
|
+
expect(classifyAutoRetryReason("API Error: Response stalled mid-stream.")).toBe(
|
|
50
|
+
AUTO_RETRY_REASONS[1],
|
|
51
|
+
);
|
|
52
|
+
expect(classifyAutoRetryReason("API Error: Connection closed mid-response.")).toBe(
|
|
53
|
+
AUTO_RETRY_REASONS[2],
|
|
54
|
+
);
|
|
55
|
+
expect(classifyAutoRetryReason("Claude usage limit reached")).toBe(AUTO_RETRY_REASONS[3]);
|
|
56
|
+
expect(classifyAutoRetryReason("You are being rate-limited")).toBe(AUTO_RETRY_REASONS[4]);
|
|
57
|
+
expect(classifyAutoRetryReason("API Error: 503 Service Unavailable")).toBe(
|
|
58
|
+
AUTO_RETRY_REASON_FALLBACK,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("formatDurationSecs", () => {
|
|
64
|
+
it("renders s / m / h forms", () => {
|
|
65
|
+
expect(formatDurationSecs(0)).toBe("0s");
|
|
66
|
+
expect(formatDurationSecs(45)).toBe("45s");
|
|
67
|
+
expect(formatDurationSecs(60)).toBe("1m");
|
|
68
|
+
expect(formatDurationSecs(200)).toBe("3m20s");
|
|
69
|
+
expect(formatDurationSecs(3600)).toBe("1h");
|
|
70
|
+
expect(formatDurationSecs(3900)).toBe("1h05m");
|
|
71
|
+
expect(formatDurationSecs(8 * 3600)).toBe("8h");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("buildAutoRetryMessage", () => {
|
|
76
|
+
it("is one line and carries attempt/reason/backoff context + ignore clause", () => {
|
|
77
|
+
const msg = buildAutoRetryMessage(3, AUTO_RETRY_REASONS[0], 45, 64);
|
|
78
|
+
expect(msg).not.toContain("\n");
|
|
79
|
+
expect(msg.startsWith("retry [")).toBe(true);
|
|
80
|
+
expect(msg).toContain("#3");
|
|
81
|
+
expect(msg).toContain("45s ago");
|
|
82
|
+
expect(msg).toContain("in 1m04s");
|
|
83
|
+
expect(msg).toContain("giving up after 8h");
|
|
84
|
+
expect(msg).toContain("ignore it");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Self-trigger guard: the message is typed into the PTY and stays visible in
|
|
88
|
+
// the transcript, so if it ever matched a CLI's own screen-scrape patterns
|
|
89
|
+
// it would re-arm the retry loop (autoRetry), block the streak reset, kill
|
|
90
|
+
// the session (fatal), or fake a state (working/enter/needsInput). Mirrors
|
|
91
|
+
// rs/src/config.rs test_auto_retry_message_is_inert_against_all_cli_patterns.
|
|
92
|
+
it("never matches any CLI's screen-scrape patterns", async () => {
|
|
93
|
+
const clis = await loadSharedCliDefaults();
|
|
94
|
+
expect(Object.keys(clis).length).toBeGreaterThan(0);
|
|
95
|
+
for (const [cliName, conf] of Object.entries(clis)) {
|
|
96
|
+
for (const reason of AUTO_RETRY_REASONS) {
|
|
97
|
+
const cases: [number, number, number][] = [
|
|
98
|
+
[1, 0, 8],
|
|
99
|
+
[5, 500, 128],
|
|
100
|
+
[12, 30_000, 256],
|
|
101
|
+
];
|
|
102
|
+
for (const [attempt, since, next] of cases) {
|
|
103
|
+
const msg = buildAutoRetryMessage(attempt, reason, since, next);
|
|
104
|
+
const groups: [string, RegExp[] | undefined][] = [
|
|
105
|
+
["autoRetry", conf.autoRetry],
|
|
106
|
+
["fatal", conf.fatal],
|
|
107
|
+
["enter", conf.enter],
|
|
108
|
+
["working", conf.working],
|
|
109
|
+
["needsInput", conf.needsInput],
|
|
110
|
+
];
|
|
111
|
+
for (const [kind, patterns] of groups) {
|
|
112
|
+
for (const rx of patterns ?? []) {
|
|
113
|
+
expect(
|
|
114
|
+
rx.test(msg),
|
|
115
|
+
`retry message must not match ${cliName}.${kind} /${rx}/: ${msg}`,
|
|
116
|
+
).toBe(false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
badgeDef,
|
|
4
|
+
badgeLabel,
|
|
5
|
+
BADGE_DEFS,
|
|
6
|
+
matchBadges,
|
|
7
|
+
TYPING_BADGE,
|
|
8
|
+
type BadgeDef,
|
|
9
|
+
} from "./badges.ts";
|
|
10
|
+
|
|
11
|
+
describe("matchBadges", () => {
|
|
12
|
+
it("matches goal-active when the /goal status line is on screen", () => {
|
|
13
|
+
const lines = ["some output", "/goal active (42s)", "more output"];
|
|
14
|
+
expect(matchBadges(lines)).toEqual(["goal-active"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("is case-insensitive", () => {
|
|
18
|
+
expect(matchBadges(["/GOAL ACTIVE (1s)"])).toEqual(["goal-active"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns empty when nothing matches", () => {
|
|
22
|
+
expect(matchBadges(["just some regular CLI output", "nothing special here"])).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns empty for an empty screen", () => {
|
|
26
|
+
expect(matchBadges([])).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("matches across a join boundary between lines (pattern spans the joined text)", () => {
|
|
30
|
+
// Sanity: matchBadges joins lines with \n before testing, so a pattern that
|
|
31
|
+
// doesn't care about line boundaries still finds a hit split across two lines.
|
|
32
|
+
const defs: BadgeDef[] = [{ id: "spans", label: "x", title: "t", pattern: /foo\nbar/ }];
|
|
33
|
+
expect(matchBadges(["foo", "bar"], defs)).toEqual(["spans"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("supports custom def sets independent of the built-in BADGE_DEFS", () => {
|
|
37
|
+
const defs: BadgeDef[] = [
|
|
38
|
+
{ id: "custom-error", label: "err", title: "custom error banner", pattern: /FATAL: boom/ },
|
|
39
|
+
];
|
|
40
|
+
expect(matchBadges(["FATAL: boom"], defs)).toEqual(["custom-error"]);
|
|
41
|
+
expect(matchBadges(["FATAL: boom"])).toEqual([]); // not in the default set
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("can match multiple badges at once, in def order", () => {
|
|
45
|
+
const defs: BadgeDef[] = [
|
|
46
|
+
{ id: "a", label: "a", title: "a", pattern: /alpha/ },
|
|
47
|
+
{ id: "b", label: "b", title: "b", pattern: /beta/ },
|
|
48
|
+
];
|
|
49
|
+
expect(matchBadges(["alpha and beta both here"], defs)).toEqual(["a", "b"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("matches session-limit when claude's usage-limit banner is on screen", () => {
|
|
53
|
+
expect(matchBadges(["You've hit your session limit · resets 9:50pm (Asia/Tokyo)"])).toEqual([
|
|
54
|
+
"session-limit",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("matches with or without the apostrophe (straight ', curly ’, or none)", () => {
|
|
59
|
+
expect(matchBadges(["You've hit your session limit"])).toEqual(["session-limit"]);
|
|
60
|
+
expect(matchBadges(["Youve hit your session limit"])).toEqual(["session-limit"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not match an unrelated 'limit' mention", () => {
|
|
64
|
+
expect(matchBadges(["rate limit exceeded, please slow down"])).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("matches retrying on claude's self-retry backoff banner", () => {
|
|
68
|
+
expect(
|
|
69
|
+
matchBadges(["✻ Waiting for API response · will retry in 2m 17s · check your network"]),
|
|
70
|
+
).toEqual(["retrying"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("matches retrying across a line-wrapped banner (joined text)", () => {
|
|
74
|
+
expect(
|
|
75
|
+
matchBadges(["✻ Waiting for API response ·", "will retry in 45s · check your network"]),
|
|
76
|
+
).toEqual(["retrying"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not light retrying for a normal in-flight wait (no 'will retry')", () => {
|
|
80
|
+
expect(matchBadges(["✻ Waiting for API response… (esc to interrupt)"])).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not light retrying for an agent merely discussing retries (anchored to the banner)", () => {
|
|
84
|
+
expect(matchBadges(["the client will retry in 5 seconds with backoff"])).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("dynamic footer counters", () => {
|
|
89
|
+
// Real footer lines captured from live agents (`ay tail`), singular and plural.
|
|
90
|
+
it("matches the shell counter and carries the footer text on the wire", () => {
|
|
91
|
+
expect(
|
|
92
|
+
matchBadges([
|
|
93
|
+
" ⏸ manual mode on · 1 shell · ctrl+t to hide tasks · ← for agents · ↓ to manage",
|
|
94
|
+
]),
|
|
95
|
+
).toEqual(["shells:1 shell"]);
|
|
96
|
+
expect(matchBadges([" ⏸ manual mode on · 4 shells · ↓ to manage"])).toEqual([
|
|
97
|
+
"shells:4 shells",
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("matches the monitor counter", () => {
|
|
102
|
+
expect(
|
|
103
|
+
matchBadges([" ⏸ manual mode on · 3 monitors · esc to interrupt · ↓ to manage"]),
|
|
104
|
+
).toEqual(["monitors:3 monitors"]);
|
|
105
|
+
expect(matchBadges([" ⏸ manual mode on · 1 monitor · ↓ to manage"])).toEqual([
|
|
106
|
+
"monitors:1 monitor",
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("matches the background-agents counter (← N agents) at end of line", () => {
|
|
111
|
+
expect(matchBadges([" ⏸ manual mode on · ? for shortcuts · ← 3 agents"])).toEqual([
|
|
112
|
+
"bg-agents:3 agents",
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not light bg-agents on the plain '← for agents' hint (no count)", () => {
|
|
117
|
+
expect(matchBadges([" ⏸ manual mode on · ctrl+t to hide tasks · ← for agents"])).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("matches the PR chip", () => {
|
|
121
|
+
expect(matchBadges([" ⏸ manual mode on · PR #310"])).toEqual(["pr:PR #310"]);
|
|
122
|
+
expect(matchBadges([" ⏸ manual mode on · PR #229 · esc to interrupt"])).toEqual([
|
|
123
|
+
"pr:PR #229",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("is anchored to footer chrome — prose mentions don't match", () => {
|
|
128
|
+
expect(
|
|
129
|
+
matchBadges([
|
|
130
|
+
"I opened 4 shells and 2 monitors while checking PR #310 today",
|
|
131
|
+
"there are 3 agents running",
|
|
132
|
+
]),
|
|
133
|
+
).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("can light several counters on one footer line", () => {
|
|
137
|
+
expect(
|
|
138
|
+
matchBadges([" ⏸ manual mode on · 2 shells · 3 monitors · PR #12 · ← 5 agents"]),
|
|
139
|
+
).toEqual(["shells:2 shells", "monitors:3 monitors", "bg-agents:5 agents", "pr:PR #12"]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("badgeDef", () => {
|
|
144
|
+
it("looks up a built-in definition by id", () => {
|
|
145
|
+
expect(badgeDef("goal-active")).toBe(BADGE_DEFS[0]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns undefined for an unknown id", () => {
|
|
149
|
+
expect(badgeDef("does-not-exist")).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("resolves a dynamic id by the part before the ':'", () => {
|
|
153
|
+
expect(badgeDef("shells:4 shells")?.id).toBe("shells");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("resolves the time-derived typing badge even though it isn't in BADGE_DEFS", () => {
|
|
157
|
+
expect(badgeDef("typing")).toBe(TYPING_BADGE);
|
|
158
|
+
expect(BADGE_DEFS).not.toContain(TYPING_BADGE);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("badgeLabel", () => {
|
|
163
|
+
it("returns the static label for a plain id", () => {
|
|
164
|
+
expect(badgeLabel("goal-active")).toBe("goal");
|
|
165
|
+
expect(badgeLabel("typing")).toBe("typing");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("substitutes the captured footer text into a dynamic label", () => {
|
|
169
|
+
expect(badgeLabel("shells:1 shell")).toBe("1 shell");
|
|
170
|
+
expect(badgeLabel("shells:4 shells")).toBe("4 shells");
|
|
171
|
+
expect(badgeLabel("pr:PR #310")).toBe("PR #310");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("falls back to the raw id for an unknown badge", () => {
|
|
175
|
+
expect(badgeLabel("some-future-flag")).toBe("some-future-flag");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("TYPING_BADGE", () => {
|
|
180
|
+
it("is never produced by screen matching (its pattern can't match)", () => {
|
|
181
|
+
// Presence is derived from the stdin-activity marker, not the rendered
|
|
182
|
+
// screen — matchBadges must never surface it, even against text mentioning
|
|
183
|
+
// typing. A screen can't cause a false 'user is typing' chip.
|
|
184
|
+
expect(matchBadges(["the user is typing a lot right now", "typing typing typing"])).toEqual([]);
|
|
185
|
+
expect(TYPING_BADGE.pattern.test("typing")).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env bun test
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { buildRustArgs } from "./buildRustArgs";
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_CLIS = [
|
|
6
|
+
"claude",
|
|
7
|
+
"glm",
|
|
8
|
+
"pi",
|
|
9
|
+
"codex",
|
|
10
|
+
"copilot",
|
|
11
|
+
"cursor",
|
|
12
|
+
"grok",
|
|
13
|
+
"qwen",
|
|
14
|
+
"auggie",
|
|
15
|
+
"amp",
|
|
16
|
+
"opencode",
|
|
17
|
+
"dsh",
|
|
18
|
+
"dsh-tui",
|
|
19
|
+
"dsh-legacy",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Helper: simulate argv as [node, script, ...userArgs]
|
|
23
|
+
function argv(...userArgs: string[]): string[] {
|
|
24
|
+
return ["node", "/path/to/claude-yes", ...userArgs];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("buildRustArgs", () => {
|
|
28
|
+
// ─── Core: CLI name passed via --cli= flag ─────────────────────────
|
|
29
|
+
// The CLI name is passed via --cli= flag at the start, so it doesn't
|
|
30
|
+
// get mixed with trailing positional args (which are prompt text).
|
|
31
|
+
|
|
32
|
+
describe("CLI name is passed via --cli= flag", () => {
|
|
33
|
+
it("prepends --cli= flag before other args", () => {
|
|
34
|
+
const result = buildRustArgs(argv("--timeout", "1h"), "claude", SUPPORTED_CLIS);
|
|
35
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "1h"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("prepends --cli= flag before multiple flags", () => {
|
|
39
|
+
const result = buildRustArgs(
|
|
40
|
+
argv("--timeout", "30s", "--verbose", "--robust", "true"),
|
|
41
|
+
"claude",
|
|
42
|
+
SUPPORTED_CLIS,
|
|
43
|
+
);
|
|
44
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "30s", "--verbose", "--robust", "true"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("--cli= is the first element", () => {
|
|
48
|
+
const result = buildRustArgs(argv("--timeout", "5m"), "codex", SUPPORTED_CLIS);
|
|
49
|
+
expect(result[0]).toBe("--cli=codex");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Regression: flags must not be swallowed ───────────────────────
|
|
54
|
+
|
|
55
|
+
describe("regression: flags are preserved alongside --cli=", () => {
|
|
56
|
+
it("--timeout is preserved", () => {
|
|
57
|
+
const result = buildRustArgs(argv("--timeout", "1h"), "claude", SUPPORTED_CLIS);
|
|
58
|
+
expect(result).toContain("--timeout");
|
|
59
|
+
expect(result).toContain("1h");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("--verbose is preserved", () => {
|
|
63
|
+
const result = buildRustArgs(argv("--verbose"), "claude", SUPPORTED_CLIS);
|
|
64
|
+
expect(result).toContain("--verbose");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("-c (continue) flag is preserved", () => {
|
|
68
|
+
const result = buildRustArgs(argv("-c"), "claude", SUPPORTED_CLIS);
|
|
69
|
+
expect(result).toContain("-c");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("all flags are preserved in complex invocation", () => {
|
|
73
|
+
const result = buildRustArgs(
|
|
74
|
+
argv("--timeout", "1h", "--verbose", "-c", "--robust", "true"),
|
|
75
|
+
"claude",
|
|
76
|
+
SUPPORTED_CLIS,
|
|
77
|
+
);
|
|
78
|
+
for (const flag of ["--timeout", "--verbose", "-c", "--robust"]) {
|
|
79
|
+
expect(result).toContain(flag);
|
|
80
|
+
}
|
|
81
|
+
expect(result[0]).toBe("--cli=claude");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ─── --rust flag filtering ─────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
describe("--rust flag is filtered out", () => {
|
|
88
|
+
it("removes --rust from args", () => {
|
|
89
|
+
const result = buildRustArgs(argv("--rust", "--timeout", "30s"), "claude", SUPPORTED_CLIS);
|
|
90
|
+
expect(result).not.toContain("--rust");
|
|
91
|
+
expect(result).toContain("--timeout");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("removes --rust= variant from args", () => {
|
|
95
|
+
const result = buildRustArgs(argv("--rust=true", "--verbose"), "claude", SUPPORTED_CLIS);
|
|
96
|
+
expect(result).not.toContain("--rust=true");
|
|
97
|
+
expect(result).toContain("--verbose");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("removes --rust regardless of position", () => {
|
|
101
|
+
const result = buildRustArgs(
|
|
102
|
+
argv("--verbose", "--rust", "--timeout", "5m"),
|
|
103
|
+
"claude",
|
|
104
|
+
SUPPORTED_CLIS,
|
|
105
|
+
);
|
|
106
|
+
expect(result).not.toContain("--rust");
|
|
107
|
+
expect(result).toEqual(["--cli=claude", "--verbose", "--timeout", "5m"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─── CLI name detection in args ────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe("does not duplicate CLI name if already in args", () => {
|
|
114
|
+
it("hoists a positional CLI name into a leading --cli= flag", () => {
|
|
115
|
+
const result = buildRustArgs(argv("--timeout", "30s", "claude"), "claude", SUPPORTED_CLIS);
|
|
116
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "30s"]);
|
|
117
|
+
expect(result.filter((a) => a.includes("claude"))).toHaveLength(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("skips --cli= when --cli= flag is already used", () => {
|
|
121
|
+
const result = buildRustArgs(
|
|
122
|
+
argv("--cli=codex", "--timeout", "30s"),
|
|
123
|
+
"claude",
|
|
124
|
+
SUPPORTED_CLIS,
|
|
125
|
+
);
|
|
126
|
+
expect(result).toEqual(["--cli=codex", "--timeout", "30s"]);
|
|
127
|
+
expect(result).not.toContain("--cli=claude");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("skips --cli= when --cli flag is used (separate value)", () => {
|
|
131
|
+
const result = buildRustArgs(
|
|
132
|
+
argv("--cli", "codex", "--timeout", "30s"),
|
|
133
|
+
"claude",
|
|
134
|
+
SUPPORTED_CLIS,
|
|
135
|
+
);
|
|
136
|
+
expect(result).toEqual(["--cli", "codex", "--timeout", "30s"]);
|
|
137
|
+
expect(result).not.toContain("--cli=claude");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("detects any supported CLI name in args", () => {
|
|
141
|
+
for (const cli of ["codex", "copilot", "cursor", "grok"]) {
|
|
142
|
+
const result = buildRustArgs(argv("--timeout", "1m", cli), "claude", SUPPORTED_CLIS);
|
|
143
|
+
expect(result).not.toContain("--cli=claude");
|
|
144
|
+
expect(result[0]).toBe(`--cli=${cli}`);
|
|
145
|
+
expect(result).toContain("--timeout");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── Regression: agent-yes flags AFTER a positional CLI name ───────
|
|
151
|
+
// `ay <cli> --cwd <dir>` is the documented form. The CLI name must be hoisted
|
|
152
|
+
// to a leading --cli= flag, otherwise clap's trailing_var_arg swallows --cwd
|
|
153
|
+
// and forwards it to the target CLI (which errors on the unknown option).
|
|
154
|
+
|
|
155
|
+
describe("regression: flags after a positional CLI name are not swallowed", () => {
|
|
156
|
+
it("hoists CLI name so --cwd is parsed by agent-yes, not forwarded", () => {
|
|
157
|
+
const result = buildRustArgs(
|
|
158
|
+
argv("claude", "--cwd", "../qa1", "--", "do", "the", "thing"),
|
|
159
|
+
"claude",
|
|
160
|
+
SUPPORTED_CLIS,
|
|
161
|
+
);
|
|
162
|
+
expect(result).toEqual(["--cli=claude", "--cwd", "../qa1", "--", "do", "the", "thing"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("works for the ay generic entry (cliFromScript undefined)", () => {
|
|
166
|
+
const result = buildRustArgs(argv("codex", "--cwd", "/tmp/x"), undefined, SUPPORTED_CLIS);
|
|
167
|
+
expect(result).toEqual(["--cli=codex", "--cwd", "/tmp/x"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("does not treat a supported-CLI word after -- as the CLI selector", () => {
|
|
171
|
+
const result = buildRustArgs(
|
|
172
|
+
argv("--cwd", "/tmp/x", "--", "ask", "claude"),
|
|
173
|
+
"claude",
|
|
174
|
+
SUPPORTED_CLIS,
|
|
175
|
+
);
|
|
176
|
+
expect(result).toEqual(["--cli=claude", "--cwd", "/tmp/x", "--", "ask", "claude"]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ─── Swarm mode ────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe("swarm mode skips CLI name", () => {
|
|
183
|
+
it("does not add --cli= when --swarm is present", () => {
|
|
184
|
+
const result = buildRustArgs(argv("--swarm", "my-topic"), "claude", SUPPORTED_CLIS);
|
|
185
|
+
expect(result).toEqual(["--swarm", "my-topic"]);
|
|
186
|
+
expect(result).not.toContain("--cli=claude");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("does not add --cli= when --swarm= is present", () => {
|
|
190
|
+
const result = buildRustArgs(argv("--swarm=my-topic"), "claude", SUPPORTED_CLIS);
|
|
191
|
+
expect(result).toEqual(["--swarm=my-topic"]);
|
|
192
|
+
expect(result).not.toContain("--cli=claude");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("does not add --cli= for bare --swarm", () => {
|
|
196
|
+
const result = buildRustArgs(argv("--swarm"), "claude", SUPPORTED_CLIS);
|
|
197
|
+
expect(result).toEqual(["--swarm"]);
|
|
198
|
+
expect(result).not.toContain("--cli=claude");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ─── No CLI from script name ───────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
describe("no CLI from script name (agent-yes / ay)", () => {
|
|
205
|
+
it("returns raw args when cliFromScript is undefined", () => {
|
|
206
|
+
const result = buildRustArgs(argv("--timeout", "30s"), undefined, SUPPORTED_CLIS);
|
|
207
|
+
expect(result).toEqual(["--timeout", "30s"]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns raw args when cliFromScript is empty string", () => {
|
|
211
|
+
const result = buildRustArgs(argv("--timeout", "30s"), "", SUPPORTED_CLIS);
|
|
212
|
+
expect(result).toEqual(["--timeout", "30s"]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── Edge cases ────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("edge cases", () => {
|
|
219
|
+
it("handles no user args at all", () => {
|
|
220
|
+
const result = buildRustArgs(["node", "/path/to/claude-yes"], "claude", SUPPORTED_CLIS);
|
|
221
|
+
expect(result).toEqual(["--cli=claude"]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("handles -- prompt separator correctly", () => {
|
|
225
|
+
const result = buildRustArgs(
|
|
226
|
+
argv("--timeout", "1h", "--", "do", "the", "thing"),
|
|
227
|
+
"claude",
|
|
228
|
+
SUPPORTED_CLIS,
|
|
229
|
+
);
|
|
230
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "1h", "--", "do", "the", "thing"]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("handles -p prompt flag", () => {
|
|
234
|
+
const result = buildRustArgs(argv("-p", "hello world"), "claude", SUPPORTED_CLIS);
|
|
235
|
+
expect(result).toEqual(["--cli=claude", "-p", "hello world"]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("preserves args with hyphen values (e.g. negative numbers)", () => {
|
|
239
|
+
const result = buildRustArgs(argv("--some-flag", "-1"), "claude", SUPPORTED_CLIS);
|
|
240
|
+
expect(result).toEqual(["--cli=claude", "--some-flag", "-1"]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("handles multiple --rust flags (all filtered)", () => {
|
|
244
|
+
const result = buildRustArgs(argv("--rust", "--rust", "--verbose"), "claude", SUPPORTED_CLIS);
|
|
245
|
+
expect(result).toEqual(["--cli=claude", "--verbose"]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("does not treat --swarm-topic as --swarm", () => {
|
|
249
|
+
const result = buildRustArgs(argv("--swarm-topic", "my-topic"), "claude", SUPPORTED_CLIS);
|
|
250
|
+
// --swarm-topic is NOT --swarm, so --cli= should still be added
|
|
251
|
+
expect(result[0]).toBe("--cli=claude");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("does not treat --cli-like strings inside values as CLI names", () => {
|
|
255
|
+
const result = buildRustArgs(argv("-p", "use claude to fix"), undefined, SUPPORTED_CLIS);
|
|
256
|
+
// "claude" appears inside a value, but since cliFromScript is undefined, nothing added
|
|
257
|
+
expect(result).toEqual(["-p", "use claude to fix"]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("bare words (prompt without --) are passed through for Rust to handle", () => {
|
|
261
|
+
const result = buildRustArgs(
|
|
262
|
+
argv("rebuild", "and", "analyze", "problems"),
|
|
263
|
+
"claude",
|
|
264
|
+
SUPPORTED_CLIS,
|
|
265
|
+
);
|
|
266
|
+
expect(result).toEqual(["--cli=claude", "rebuild", "and", "analyze", "problems"]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ─── Real-world command scenarios ──────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe("real-world scenarios", () => {
|
|
273
|
+
it("claude-yes --rust --timeout 1h (the original failing case)", () => {
|
|
274
|
+
const result = buildRustArgs(argv("--rust", "--timeout", "1h"), "claude", SUPPORTED_CLIS);
|
|
275
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "1h"]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("claude-yes --rust --timeout 30s --verbose", () => {
|
|
279
|
+
const result = buildRustArgs(
|
|
280
|
+
argv("--rust", "--timeout", "30s", "--verbose"),
|
|
281
|
+
"claude",
|
|
282
|
+
SUPPORTED_CLIS,
|
|
283
|
+
);
|
|
284
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "30s", "--verbose"]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("claude-yes --rust -c (continue session)", () => {
|
|
288
|
+
const result = buildRustArgs(argv("--rust", "-c"), "claude", SUPPORTED_CLIS);
|
|
289
|
+
expect(result).toEqual(["--cli=claude", "-c"]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("codex-yes --rust --timeout 5m -- fix all bugs", () => {
|
|
293
|
+
const result = buildRustArgs(
|
|
294
|
+
argv("--rust", "--timeout", "5m", "--", "fix", "all", "bugs"),
|
|
295
|
+
"codex",
|
|
296
|
+
SUPPORTED_CLIS,
|
|
297
|
+
);
|
|
298
|
+
expect(result).toEqual(["--cli=codex", "--timeout", "5m", "--", "fix", "all", "bugs"]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("agent-yes --rust claude --timeout 1h (explicit CLI in args)", () => {
|
|
302
|
+
const result = buildRustArgs(
|
|
303
|
+
argv("--rust", "claude", "--timeout", "1h"),
|
|
304
|
+
undefined,
|
|
305
|
+
SUPPORTED_CLIS,
|
|
306
|
+
);
|
|
307
|
+
// cliFromScript is undefined (agent-yes); the positional CLI name is hoisted
|
|
308
|
+
// to --cli= so --timeout isn't swallowed by clap's trailing_var_arg.
|
|
309
|
+
expect(result).toEqual(["--cli=claude", "--timeout", "1h"]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("agent-yes --rust --swarm my-project --timeout 1h", () => {
|
|
313
|
+
const result = buildRustArgs(
|
|
314
|
+
argv("--rust", "--swarm", "my-project", "--timeout", "1h"),
|
|
315
|
+
"claude",
|
|
316
|
+
SUPPORTED_CLIS,
|
|
317
|
+
);
|
|
318
|
+
// Swarm mode: no CLI name added
|
|
319
|
+
expect(result).toEqual(["--swarm", "my-project", "--timeout", "1h"]);
|
|
320
|
+
expect(result).not.toContain("--cli=claude");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("codex-yes --rust --timeout 2m --verbose -p 'hello'", () => {
|
|
324
|
+
const result = buildRustArgs(
|
|
325
|
+
argv("--rust", "--timeout", "2m", "--verbose", "-p", "hello"),
|
|
326
|
+
"codex",
|
|
327
|
+
SUPPORTED_CLIS,
|
|
328
|
+
);
|
|
329
|
+
expect(result).toEqual(["--cli=codex", "--timeout", "2m", "--verbose", "-p", "hello"]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("claude-yes --rust --auto=no --timeout 10m", () => {
|
|
333
|
+
const result = buildRustArgs(
|
|
334
|
+
argv("--rust", "--auto=no", "--timeout", "10m"),
|
|
335
|
+
"claude",
|
|
336
|
+
SUPPORTED_CLIS,
|
|
337
|
+
);
|
|
338
|
+
expect(result).toEqual(["--cli=claude", "--auto=no", "--timeout", "10m"]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("cy rebuild and analyze problems (bare prompt words)", () => {
|
|
342
|
+
const result = buildRustArgs(
|
|
343
|
+
argv("rebuild", "and", "analyze", "problems"),
|
|
344
|
+
"claude",
|
|
345
|
+
SUPPORTED_CLIS,
|
|
346
|
+
);
|
|
347
|
+
// CLI passed via --cli=, bare words passed through for Rust to interpret as prompt
|
|
348
|
+
expect(result).toEqual(["--cli=claude", "rebuild", "and", "analyze", "problems"]);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|