mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
6d58837f4f
* fix(proxy): disclose what enabling cloud routing actually decides, and let users pin a tier Closes the terminal half of cognitum-one/meta-proxy#43 M5b, and adds the `routing_mode` control ruflo never had. meta-proxy ADR-321 rev-2 (shipped v0.6.0, #43 M2) applies tier selection to ALL Cloud traffic. Before it, a deliberately configured Cloud plane forwarded the client's own model name -- and since Claude Code names a frontier model by default, every request was served at the top tier with no ceiling and no proxy daily cap. Difficulty-routing all Cloud traffic is the fix for that defect, not a regression, and it deliberately does not touch Passthrough or Local: those are the user's own subscription and their own backend. But ruflo never told anyone. `ruflo proxy config --cloud` said only that prompts go to api.cognitum.one "and forwarded to the selected provider" -- so a user could enable cloud routing still believing their model choice held. #43 M5a fixed the Developer Console selector; M5b ("equivalent disclosure to any terminal flow that explicitly activates Cloud") is this command, and it was still open. The disclosure now answers the three things activating Cloud actually decides, each of which has a different answer than the plane being left: - who processes the prompt (Cognitum, server-side) - who pays (the Cognitum account -- NOT the user's own Claude subscription, which is what Passthrough uses and is unaffected) - which model runs (the plane picks a tier per prompt rather than honoring the requested model) `--routing-mode <auto|low|mid|high>` is rev-2's escape hatch. The console got a control for it in meta-proxy#52; ruflo had none, so a ruflo user was stuck on auto with no way to disagree -- and the scorer reads prompt SHAPE, not task difficulty, so a short-but-hard prompt can under-escalate. Setting a tier must never activate Cloud (ADR-321 Revision 3 keeps the plane choice and this Cloud-only setting as separate controls), so `--routing-mode` alone writes only `routing_mode`, asks for no consent it does not need, and says the setting is inert until Cloud is on. `--routing-mode` with `--local-only` is refused rather than silently resolved, and an unconfirmed `--cloud` still writes nothing at all. Wire values confirmed against meta-proxy's `RoutingMode` enum (`src/config.rs`, same `#[serde(rename_all = "snake_case")]` treatment as `DataPlane`), and `#[serde(default)]` there means an existing config that omits the field keeps working and reads as auto. An unrecognized value already in the file reports as auto rather than being echoed back, matching the proxy's own degrade-to-safe-default behavior. ADR-304 records both, and drops its inline copy of the disclosure text -- that copy had already fallen behind the shipped string, and a reader could not tell which was real. #43 M3 (a Cloud tier ceiling and daily cap) stays open upstream as a product decision, so the disclosure makes no claim about caps either way. Test: 9 proxy test files, 68 tests, all passing (was 56). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): make the subscription plane reachable again after cloud routing `--local-only` was the only exit from the cloud plane, and it writes `default_data_plane = "local"` -- the user's own Ollama/vLLM/SGLang backend. `passthrough`, which is meta-proxy's own default AND the plane that uses the user's own Claude subscription, was unreachable from this command by design ("passthrough is never written", ADR-304's 07-16 addendum). So the undo we advertised was wrong. A user on passthrough who followed our own disclosure -- "Disable anytime: ruflo proxy config --local-only" -- landed on a THIRD state they never chose, pointed at a local backend that may not be installed. Two consequences, neither visible: 1. Their Claude subscription stops being used at all. That is the same harm cognitum-one/meta-proxy#51 closed on the login path ("silently switches a user away from Passthrough -- their own Claude subscription"), reached through a different door. 2. Automatic quota failover silently stops applying. meta-proxy gates it on the plane (`src/routing.rs`: `automatic_eligible = cfg.default_data_plane == DataPlane::Passthrough`) because Passthrough is the only plane that sees Anthropic's own rate-limit headers (ADR-320). A user parked on `local` is opted out of ADR-321 entirely and is never told. The no-flag report papered over the difference, printing one line -- "Cloud routing is OFF -- requests never leave this machine (or use your own Claude subscription on Passthrough)" -- for two planes that do not behave alike. `--local-only` keeps its meaning exactly; the flag name and ADR-304 both promise a local backend, and silently retargeting it would just be a second surprise. Instead: - `--passthrough` is added, writing `default_data_plane = "passthrough"` and revoking cloud-routing consent the same way `--local-only` does. - The disclosure names both exits instead of presenting one as THE undo. - `--cloud` reads the plane it is leaving before overwriting it and prints the command to restore it. That read is the only moment ruflo knows where the user was, and it beats persisting a "previous plane" that could go stale against a hand-edited TOML. - `--local-only` now says the Claude subscription is not used on that plane, and how to choose it instead. - The no-flag report describes each plane distinctly and calls out the ADR-321 failover consequence while on `local`. - Plane flags are mutually exclusive -- more than one is refused rather than resolved by precedence, since any precedence would discard something the user explicitly asked for. No default changes: a user who never runs this command is on passthrough, exactly as before. ADR-304 records the decision and supersedes its own "passthrough is never written" claim. Test: 9 proxy test files, 80 tests, all passing (was 68). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): correct disclosure's who-pays claim (post-merge reapply) Same fix as on #2927 — the disclosure asserted Passthrough "is the plane in effect until you turn this on", which is false whenever the user is already on --local-only. Reapplying after merging main (which already had this fix on the pre-passthrough disclosure text) since this branch's own commit still carried the original wording. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: ruv <ruv@ruv.net>
422 lines
17 KiB
TypeScript
422 lines
17 KiB
TypeScript
/**
|
|
* `proxy config --cloud/--local-only` (ADR-304). The `default_data_plane`
|
|
* TOML values written here ("local"/"cloud") were confirmed against
|
|
* meta-proxy's actual `DataPlane` enum (`src/config.rs`,
|
|
* `#[serde(rename_all = "snake_case")]`) both by reading the source directly
|
|
* and by a live behavioral test against the real v0.1.0 binary.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import type { CommandContext } from '../src/types.js';
|
|
|
|
let stateDir: string;
|
|
let savedEnv: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(() => {
|
|
stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-config-cmd-test-'));
|
|
savedEnv = { ...process.env };
|
|
process.env.RUFLO_STATE_DIR = stateDir;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = savedEnv;
|
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function ctxWithFlags(flags: Record<string, unknown>): CommandContext {
|
|
return { args: [], flags: { _: [], ...flags }, cwd: process.cwd(), interactive: false };
|
|
}
|
|
|
|
async function getConfigSub() {
|
|
const { proxyCommand } = await import('../src/commands/proxy.js');
|
|
const sub = proxyCommand.subcommands?.find((c) => c.name === 'config');
|
|
if (!sub) throw new Error('config subcommand not found');
|
|
return sub;
|
|
}
|
|
|
|
describe('proxy config', () => {
|
|
it('with no flags, reports the default plane (passthrough) when no config file exists', async () => {
|
|
const configSub = await getConfigSub();
|
|
const result = await configSub.action!(ctxWithFlags({}));
|
|
expect(result?.success).toBe(true);
|
|
expect((result?.data as { plane?: string })?.plane).toBe('passthrough');
|
|
});
|
|
|
|
it('rejects --cloud and --local-only together', async () => {
|
|
const configSub = await getConfigSub();
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true, localOnly: true }));
|
|
expect(result?.success).toBe(false);
|
|
});
|
|
|
|
it('--cloud without --yes shows the disclosure and writes nothing', async () => {
|
|
const configSub = await getConfigSub();
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true }));
|
|
expect(result?.success).toBe(true);
|
|
expect((result?.data as { confirmed?: boolean })?.confirmed).toBe(false);
|
|
expect(fs.existsSync(path.join(stateDir, 'proxy-config.toml'))).toBe(false);
|
|
|
|
const { hasConsent } = await import('../src/funnel/index.js');
|
|
expect(hasConsent('cloud-routing')).toBe(false);
|
|
});
|
|
|
|
it('--cloud --yes writes default_data_plane = "cloud" and grants consent', async () => {
|
|
const configSub = await getConfigSub();
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
expect(result?.success).toBe(true);
|
|
|
|
const raw = fs.readFileSync(path.join(stateDir, 'proxy-config.toml'), 'utf-8');
|
|
expect(raw).toContain('default_data_plane = "cloud"');
|
|
|
|
const { hasConsent } = await import('../src/funnel/index.js');
|
|
expect(hasConsent('cloud-routing')).toBe(true);
|
|
});
|
|
|
|
it('--local-only writes default_data_plane = "local" and revokes consent', async () => {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
await configSub.action!(ctxWithFlags({ localOnly: true }));
|
|
|
|
const raw = fs.readFileSync(path.join(stateDir, 'proxy-config.toml'), 'utf-8');
|
|
expect(raw).toContain('default_data_plane = "local"');
|
|
|
|
const { hasConsent } = await import('../src/funnel/index.js');
|
|
expect(hasConsent('cloud-routing')).toBe(false);
|
|
});
|
|
|
|
it('revoking via --local-only means a later --cloud (no --yes) re-shows the disclosure', async () => {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
await configSub.action!(ctxWithFlags({ localOnly: true }));
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true }));
|
|
expect((result?.data as { confirmed?: boolean })?.confirmed).toBe(false);
|
|
});
|
|
|
|
it('preserves unrelated lines already in proxy-config.toml (read-modify-write, not overwrite)', async () => {
|
|
fs.writeFileSync(
|
|
path.join(stateDir, 'proxy-config.toml'),
|
|
'bind = "127.0.0.1:11435"\ndefault_data_plane = "local"\nsponsored_daily_cap_usd = 5.0\n',
|
|
);
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
|
|
const raw = fs.readFileSync(path.join(stateDir, 'proxy-config.toml'), 'utf-8');
|
|
expect(raw).toContain('bind = "127.0.0.1:11435"');
|
|
expect(raw).toContain('sponsored_daily_cap_usd = 5.0');
|
|
expect(raw).toContain('default_data_plane = "cloud"');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* `--routing-mode` (meta-proxy ADR-321 rev-2). Values match the Rust
|
|
* `RoutingMode` enum. The invariant worth pinning is ADR-321 Revision 3's:
|
|
* the plane choice and this Cloud-only tier setting are SEPARATE controls,
|
|
* so selecting a tier must never activate Cloud.
|
|
*/
|
|
describe('proxy config --routing-mode', () => {
|
|
const configPath = () => path.join(stateDir, 'proxy-config.toml');
|
|
|
|
it('writes routing_mode without touching the data plane or granting consent', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ routingMode: 'high' }));
|
|
|
|
expect(result?.success).toBe(true);
|
|
const raw = fs.readFileSync(configPath(), 'utf-8');
|
|
expect(raw).toContain('routing_mode = "high"');
|
|
expect(raw).not.toContain('default_data_plane');
|
|
|
|
const { hasConsent } = await import('../src/funnel/index.js');
|
|
expect(hasConsent('cloud-routing')).toBe(false);
|
|
});
|
|
|
|
it('accepts every mode meta-proxy defines', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
for (const mode of ['auto', 'low', 'mid', 'high']) {
|
|
const result = await configSub.action!(ctxWithFlags({ routingMode: mode }));
|
|
expect(result?.success, mode).toBe(true);
|
|
expect(fs.readFileSync(configPath(), 'utf-8')).toContain(`routing_mode = "${mode}"`);
|
|
}
|
|
});
|
|
|
|
it('rejects a mode meta-proxy would not understand, and writes nothing', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ routingMode: 'ultra' }));
|
|
|
|
expect(result?.success).toBe(false);
|
|
expect(fs.existsSync(configPath())).toBe(false);
|
|
});
|
|
|
|
it('refuses to pin a tier while --local-only is turning the cloud plane off', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ localOnly: true, routingMode: 'high' }));
|
|
|
|
expect(result?.success).toBe(false);
|
|
expect(fs.existsSync(configPath())).toBe(false);
|
|
});
|
|
|
|
it('applies alongside --cloud --yes in one command', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true, yes: true, routingMode: 'low' }));
|
|
|
|
expect(result?.success).toBe(true);
|
|
const raw = fs.readFileSync(configPath(), 'utf-8');
|
|
expect(raw).toContain('default_data_plane = "cloud"');
|
|
expect(raw).toContain('routing_mode = "low"');
|
|
});
|
|
|
|
/** The unconfirmed path's whole promise is that nothing was written. */
|
|
it('writes nothing when passed with an unconfirmed --cloud', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ cloud: true, routingMode: 'low' }));
|
|
|
|
expect((result?.data as { confirmed?: boolean })?.confirmed).toBe(false);
|
|
expect(fs.existsSync(configPath())).toBe(false);
|
|
});
|
|
|
|
it('reports the current mode with no flags, defaulting to auto when unset', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const unset = await configSub.action!(ctxWithFlags({}));
|
|
expect((unset?.data as { routingMode?: string })?.routingMode).toBe('auto');
|
|
|
|
await configSub.action!(ctxWithFlags({ routingMode: 'mid' }));
|
|
const set = await configSub.action!(ctxWithFlags({}));
|
|
expect((set?.data as { routingMode?: string })?.routingMode).toBe('mid');
|
|
});
|
|
|
|
it('treats an unrecognized routing_mode already in the file as auto', async () => {
|
|
fs.writeFileSync(configPath(), 'routing_mode = "ultra"\n');
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({}));
|
|
|
|
expect((result?.data as { routingMode?: string })?.routingMode).toBe('auto');
|
|
});
|
|
|
|
it('preserves unrelated lines when writing routing_mode', async () => {
|
|
fs.writeFileSync(configPath(), 'bind = "127.0.0.1:11435"\nsponsored_daily_cap_usd = 5.0\n');
|
|
const configSub = await getConfigSub();
|
|
|
|
await configSub.action!(ctxWithFlags({ routingMode: 'high' }));
|
|
|
|
const raw = fs.readFileSync(configPath(), 'utf-8');
|
|
expect(raw).toContain('bind = "127.0.0.1:11435"');
|
|
expect(raw).toContain('sponsored_daily_cap_usd = 5.0');
|
|
expect(raw).toContain('routing_mode = "high"');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* meta-proxy#43 M5b — "add equivalent disclosure to any terminal flow that
|
|
* explicitly activates Cloud". The console got this in M5a (#60); this
|
|
* command is the terminal flow, and these are the three questions
|
|
* activating Cloud actually decides.
|
|
*/
|
|
describe('cloud routing disclosure', () => {
|
|
async function disclosureShownOnUnconfirmedCloud(): Promise<string> {
|
|
const lines: string[] = [];
|
|
const { output } = await import('../src/output.js');
|
|
const spy = vi.spyOn(output, 'writeln').mockImplementation((line?: string) => {
|
|
lines.push(String(line ?? ''));
|
|
});
|
|
try {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ cloud: true }));
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
it('says who processes the prompts', async () => {
|
|
expect(await disclosureShownOnUnconfirmedCloud()).toContain('api.cognitum.one');
|
|
});
|
|
|
|
it("says who pays, and that Passthrough uses the user's own subscription instead", async () => {
|
|
const text = await disclosureShownOnUnconfirmedCloud();
|
|
|
|
expect(text).toContain('metered against your Cognitum account');
|
|
expect(text).toContain('Passthrough');
|
|
});
|
|
|
|
/**
|
|
* The omission that mattered: on the Cloud plane the client's requested
|
|
* model is deliberately not used, and before this the disclosure never
|
|
* said so — a user could enable Cloud believing their model choice held.
|
|
*/
|
|
it('says the plane picks the tier rather than using the requested model, and how to override', async () => {
|
|
const text = await disclosureShownOnUnconfirmedCloud();
|
|
|
|
expect(text).toContain('instead of using the model');
|
|
expect(text).toContain('ruflo proxy config --routing-mode');
|
|
});
|
|
|
|
/**
|
|
* "Disable anytime: --local-only" was a single command for a choice with
|
|
* two destinations, and the one it named silently drops the user's own
|
|
* subscription. The disclosure must offer both.
|
|
*/
|
|
it('offers both ways off the cloud plane, not just the local one', async () => {
|
|
const text = await disclosureShownOnUnconfirmedCloud();
|
|
|
|
expect(text).toContain('ruflo proxy config --passthrough');
|
|
expect(text).toContain('ruflo proxy config --local-only');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* `--passthrough`. Turning cloud routing OFF is a choice of destination:
|
|
* `local` is the user's own backend, `passthrough` is their own Claude
|
|
* subscription (meta-proxy's `DataPlane::Passthrough`, and its own default).
|
|
* Before this, `--local-only` was the only exit, so a user who started on
|
|
* passthrough, enabled cloud, and turned it back off silently lost their
|
|
* subscription — and with it automatic quota failover, which meta-proxy
|
|
* (`routing.rs`) only applies while the plane is passthrough.
|
|
*/
|
|
describe('proxy config --passthrough', () => {
|
|
const configPath = () => path.join(stateDir, 'proxy-config.toml');
|
|
const planeInFile = () => fs.readFileSync(configPath(), 'utf-8').match(/default_data_plane = "([^"]*)"/)?.[1];
|
|
|
|
it('writes default_data_plane = "passthrough" and revokes cloud-routing consent', async () => {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ passthrough: true }));
|
|
|
|
expect(result?.success).toBe(true);
|
|
expect(planeInFile()).toBe('passthrough');
|
|
|
|
const { hasConsent } = await import('../src/funnel/index.js');
|
|
expect(hasConsent('cloud-routing')).toBe(false);
|
|
});
|
|
|
|
/** The defect, stated as a round trip: leave passthrough, come back to it. */
|
|
it('restores the subscription plane after a cloud round trip', async () => {
|
|
fs.writeFileSync(configPath(), 'default_data_plane = "passthrough"\n');
|
|
const configSub = await getConfigSub();
|
|
|
|
await configSub.action!(ctxWithFlags({ cloud: true, yes: true }));
|
|
expect(planeInFile()).toBe('cloud');
|
|
await configSub.action!(ctxWithFlags({ passthrough: true }));
|
|
|
|
expect(planeInFile()).toBe('passthrough');
|
|
});
|
|
|
|
it('--local-only still means the local backend, unchanged', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
await configSub.action!(ctxWithFlags({ localOnly: true }));
|
|
|
|
expect(planeInFile()).toBe('local');
|
|
});
|
|
|
|
it('refuses more than one plane flag rather than silently picking one', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
for (const flags of [
|
|
{ cloud: true, passthrough: true },
|
|
{ localOnly: true, passthrough: true },
|
|
{ cloud: true, localOnly: true, passthrough: true },
|
|
]) {
|
|
const result = await configSub.action!(ctxWithFlags(flags));
|
|
expect(result?.success, JSON.stringify(flags)).toBe(false);
|
|
}
|
|
expect(fs.existsSync(configPath())).toBe(false);
|
|
});
|
|
|
|
it('refuses to pin a tier while --passthrough is turning the cloud plane off', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
const result = await configSub.action!(ctxWithFlags({ passthrough: true, routingMode: 'high' }));
|
|
|
|
expect(result?.success).toBe(false);
|
|
expect(fs.existsSync(configPath())).toBe(false);
|
|
});
|
|
|
|
it('reports passthrough distinctly from local, not as one "cloud is off" state', async () => {
|
|
const configSub = await getConfigSub();
|
|
|
|
await configSub.action!(ctxWithFlags({ passthrough: true }));
|
|
const onPassthrough = await configSub.action!(ctxWithFlags({}));
|
|
await configSub.action!(ctxWithFlags({ localOnly: true }));
|
|
const onLocal = await configSub.action!(ctxWithFlags({}));
|
|
|
|
expect((onPassthrough?.data as { plane?: string })?.plane).toBe('passthrough');
|
|
expect((onLocal?.data as { plane?: string })?.plane).toBe('local');
|
|
});
|
|
|
|
it('preserves unrelated lines when writing the plane', async () => {
|
|
fs.writeFileSync(configPath(), 'bind = "127.0.0.1:11435"\nrouting_mode = "high"\n');
|
|
const configSub = await getConfigSub();
|
|
|
|
await configSub.action!(ctxWithFlags({ passthrough: true }));
|
|
|
|
const raw = fs.readFileSync(configPath(), 'utf-8');
|
|
expect(raw).toContain('bind = "127.0.0.1:11435"');
|
|
expect(raw).toContain('routing_mode = "high"');
|
|
expect(raw).toContain('default_data_plane = "passthrough"');
|
|
});
|
|
});
|
|
|
|
describe('plane guidance', () => {
|
|
async function linesFrom(flags: Record<string, unknown>): Promise<string> {
|
|
const lines: string[] = [];
|
|
const { output } = await import('../src/output.js');
|
|
const write = vi.spyOn(output, 'writeln').mockImplementation((line?: string) => {
|
|
lines.push(String(line ?? ''));
|
|
});
|
|
const success = vi.spyOn(output, 'printSuccess').mockImplementation((line?: string) => {
|
|
lines.push(String(line ?? ''));
|
|
});
|
|
try {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags(flags));
|
|
} finally {
|
|
write.mockRestore();
|
|
success.mockRestore();
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
it('names the plane being left and how to restore it when enabling cloud', async () => {
|
|
fs.writeFileSync(path.join(stateDir, 'proxy-config.toml'), 'default_data_plane = "passthrough"\n');
|
|
|
|
const text = await linesFrom({ cloud: true, yes: true });
|
|
|
|
expect(text).toContain('Previous plane: passthrough');
|
|
expect(text).toContain('ruflo proxy config --passthrough');
|
|
});
|
|
|
|
it('warns that --local-only does not use the Claude subscription', async () => {
|
|
const text = await linesFrom({ localOnly: true });
|
|
|
|
expect(text).toContain('NOT used on this plane');
|
|
expect(text).toContain('ruflo proxy config --passthrough');
|
|
});
|
|
|
|
it('does not add that warning when landing on passthrough', async () => {
|
|
const text = await linesFrom({ passthrough: true });
|
|
|
|
expect(text).not.toContain('NOT used on this plane');
|
|
});
|
|
|
|
/** Sitting on `local` opts you out of ADR-321 failover; nothing else says so. */
|
|
it('flags that automatic quota failover does not apply while on local', async () => {
|
|
const configSub = await getConfigSub();
|
|
await configSub.action!(ctxWithFlags({ localOnly: true }));
|
|
|
|
const text = await linesFrom({});
|
|
|
|
expect(text).toContain('automatic quota failover (ADR-321) applies only on passthrough');
|
|
});
|
|
});
|