You are offline
Reconnect and refresh, or open a page you have visited before.
Reconnect and refresh, or open a page you have visited before.
- {description} -
-- {toolDesc} -
- - ) - })} -{section.desc}
))} ++ Byteflow processing stays in your browser. Some preferences and non-sensitive tool settings may be saved locally; + sensitive payloads are configured not to persist by default. +
+{message}
: null} ++ {group.description} +
++ {tool.description} +
+ + ))} +{labels.noResultsSuggestion}
+ +External network notice
+{message}
++ {text("drag_drop_import_hint")} +
+ +- {text("drag_drop_import_hint")} -
- -{text("templates_description")}
-{text(template.titleKey)}
-{text(template.descriptionKey)}
-{storageMessage}
: null} -| {text("table_step")} | -{text("table_status")} | -{text("table_bytes")} | -{text("table_message")} | -
|---|---|---|---|
| {step.stepId} | -{step.ok ? text("status_ok") : text("status_failed")} | -{step.inputBytes} {"->"} {step.outputBytes} | -- {step.error?.message || step.warnings.join("; ") || text("no_message")} - | -
| {text("table_step")} | +{text("table_status")} | +{text("table_bytes")} | +{text("table_message")} | +
|---|---|---|---|
| {step.stepId} | +{step.ok ? text("status_ok") : text("status_failed")} | +{step.inputBytes} {"->"} {step.outputBytes} | ++ {step.error?.message || step.warnings.join("; ") || text("no_message")} + | +
{storageMessage}
: null} +{text("templates_description")}
+{text(template.titleKey)}
+{text(template.descriptionKey)}
+Hello
")).resolves.toMatchObject({ + ok: true, + finalOutput: "# Title\n\nHello", + }) + }) + + it("returns structured errors for phase-two adapter invalid input and options", async () => { + const badHashOptions = validateRecipe(buildRecipe({ + steps: [{ + id: "hash", + toolKey: "hash_generator", + adapterVersion: 1, + inputMode: "previous_output", + options: { algorithm: "bcrypt" }, + }], + edges: [], + })) + const badJwt = await runRecipe(buildRecipe({ + steps: [{ + id: "jwt", + toolKey: "jwt_decoder", + adapterVersion: 1, + inputMode: "previous_output", + options: { part: "payload" }, + }], + edges: [], + }), "not-a-jwt") + const badTimestamp = await runRecipe(buildRecipe({ + steps: [{ + id: "time", + toolKey: "unix_timestamp", + adapterVersion: 1, + inputMode: "previous_output", + options: { output: "iso" }, + }], + edges: [], + }), "not-a-timestamp") + + expect(badHashOptions.ok).toBe(false) + expect(badHashOptions.errors).toContain("hash: algorithm must be md5, sha1, sha224, sha256, sha384, or sha512.") + expect(badJwt.ok).toBe(false) + expect(badJwt.steps[0].error?.code).toBe("jwt_decode_error") + expect(badTimestamp.ok).toBe(false) + expect(badTimestamp.steps[0].error?.code).toBe("timestamp_parse_error") + }) + + it("runs regex summary and env parser adapters", async () => { + const regexRecipe = buildRecipe({ + steps: [{ + id: "regex", + toolKey: "regex_tester", + adapterVersion: 1, + inputMode: "previous_output", + options: { pattern: "([A-Z][a-z]+)(\\d)", flags: "g", maxMatches: 10 }, + }], + edges: [], + }) + const envRecipe = buildRecipe({ + steps: [{ + id: "env", + toolKey: "env_parser", + adapterVersion: 1, + inputMode: "previous_output", + options: { format: "json" }, + }], + edges: [], + }) + + const regexResult = await runRecipe(regexRecipe, "Ab1 Cd2") + const envResult = await runRecipe(envRecipe, "PORT=3000\nSECRET=\"quoted value\"") + + expect(regexResult.ok).toBe(true) + expect(JSON.parse(regexResult.finalOutput)).toMatchObject({ + count: 2, + limited: false, + matches: [ + { match: "Ab1", index: 0, groupIndex: 0, groups: ["Ab", "1"] }, + { match: "Cd2", index: 4, groupIndex: 1, groups: ["Cd", "2"] }, + ], + }) + expect(envResult).toMatchObject({ + ok: true, + finalOutput: "{\n \"PORT\": \"3000\",\n \"SECRET\": \"quoted value\"\n}", + }) + }) + + it("rejects invalid regex and env adapter options", () => { + const badRegex = validateRecipe(buildRecipe({ + steps: [{ + id: "regex", + toolKey: "regex_tester", + adapterVersion: 1, + inputMode: "previous_output", + options: { pattern: "", flags: "g", maxMatches: 10 }, + }], + edges: [], + })) + const badEnv = validateRecipe(buildRecipe({ + steps: [{ + id: "env", + toolKey: "env_parser", + adapterVersion: 1, + inputMode: "previous_output", + options: { format: "xml" }, + }], + edges: [], + })) + + expect(badRegex.ok).toBe(false) + expect(badRegex.errors).toContain("regex: pattern is required.") + expect(badEnv.ok).toBe(false) + expect(badEnv.errors).toContain("env: format must be json, yaml, or docker-args.") + }) + it("allows empty edges and executes recipe.steps order", async () => { const result = await runRecipe(buildRecipe({ edges: [] }), '{ "name": "byteflow" }') @@ -144,6 +432,35 @@ describe("pipeline foundation", () => { expect(result.finalOutput).toBe("eyJuYW1lIjoiYnl0ZWZsb3cifQ") }) + it("executes explicit linear edges from the only start step instead of array order", async () => { + const recipe = buildRecipe({ + steps: [ + { + id: "encode", + toolKey: "base64_encode_decode", + adapterVersion: 1, + inputMode: "previous_output", + options: { operation: "encode", urlSafe: true }, + }, + { + id: "format", + toolKey: "json_formatter", + adapterVersion: 1, + inputMode: "previous_output", + options: { mode: "minify" }, + }, + ], + edges: [ + { fromStepId: "format", toStepId: "encode" }, + ], + }) + const result = await runRecipe(recipe, '{ "name": "byteflow" }') + + expect(result.ok).toBe(true) + expect(result.steps.map((step) => step.stepId)).toEqual(["format", "encode"]) + expect(result.finalOutput).toBe("eyJuYW1lIjoiYnl0ZWZsb3cifQ") + }) + it("accepts a valid explicit linear chain", () => { const recipe = buildRecipe({ steps: [ @@ -204,6 +521,19 @@ describe("pipeline foundation", () => { expect(result.errors).toContain(expectedError) }) + it("rejects edges that reference unknown step ids", () => { + const result = validateRecipe(buildRecipe({ + edges: [ + { fromStepId: "format", toStepId: "missing" }, + { fromStepId: "also_missing", toStepId: "encode" }, + ], + })) + + expect(result.ok).toBe(false) + expect(result.errors).toContain("Edge references unknown toStepId: missing.") + expect(result.errors).toContain("Edge references unknown fromStepId: also_missing.") + }) + it("stops on error when configured", async () => { const result = await runRecipe(buildRecipe(), "{broken json") @@ -232,6 +562,10 @@ describe("pipeline foundation", () => { version: 1, inputKind: "text", outputKind: "text", + safeForSensitiveInput: true, + deterministic: true, + mayIncreaseSize: false, + warnings: [], defaultOptions: {}, publicOptionKeys: [], validateOptions: () => ({ ok: true, errors: [] }), @@ -257,6 +591,66 @@ describe("pipeline foundation", () => { expect(result.steps[0].error?.code).toBe("adapter_runtime_error") }) + it("rejects oversized initial input before running adapters", async () => { + const result = await runRecipe( + buildRecipe({ + settings: { + ...DEFAULT_RECIPE_SETTINGS, + maxInputBytes: 4, + }, + }), + "too large", + ) + + expect(result.ok).toBe(false) + expect(result.steps).toEqual([]) + expect(result.errors).toContain("Initial input exceeds 4 bytes.") + expect(result.finalOutput).toBe("too large") + }) + + it("stops when a step output exceeds the configured output budget", async () => { + const result = await runRecipe( + buildRecipe({ + steps: [ + { + id: "encode", + toolKey: "base64_encode_decode", + adapterVersion: 1, + inputMode: "previous_output", + options: { operation: "encode", urlSafe: true }, + }, + ], + edges: [], + settings: { + ...DEFAULT_RECIPE_SETTINGS, + maxOutputBytes: 4, + }, + }), + "byteflow", + ) + + expect(result.ok).toBe(false) + expect(result.steps).toHaveLength(1) + expect(result.steps[0].error?.code).toBe("output_too_large") + expect(result.errors).toContain("Step encode output exceeds 4 bytes.") + }) + + it("omits intermediate output fields when configured", async () => { + const result = await runRecipe( + buildRecipe({ + settings: { + ...DEFAULT_RECIPE_SETTINGS, + keepIntermediateOutputs: false, + }, + }), + '{ "name": "byteflow" }', + ) + + expect(result.ok).toBe(true) + expect(result.finalOutput).toBe("eyJuYW1lIjoiYnl0ZWZsb3cifQ") + expect(result.steps.every((step) => !Object.prototype.hasOwnProperty.call(step, "output"))).toBe(true) + }) + it("uses constant input without leaking it into default share URLs", () => { const recipe = buildRecipe({ steps: [ @@ -283,6 +677,29 @@ describe("pipeline foundation", () => { } }) + it("keeps constant input only when share URLs explicitly include runtime input", () => { + const recipe = buildRecipe({ + steps: [ + { + id: "secret_sample", + toolKey: "log_scrubber", + adapterVersion: 1, + inputMode: "constant", + constantInput: "Authorization: Bearer secret-token-value", + options: {}, + }, + ], + edges: [], + }) + const decoded = decodeRecipeFromUrlParam(encodeRecipeForShareUrl(recipe, { includeRuntimeInput: true })) + + expect(decoded.ok).toBe(true) + if (decoded.ok) { + expect(decoded.recipe.steps[0].inputMode).toBe("constant") + expect(decoded.recipe.steps[0].constantInput).toBe("Authorization: Bearer secret-token-value") + } + }) + it("removes non-public options from share URLs", () => { const recipe = buildRecipe({ steps: [ @@ -314,6 +731,45 @@ describe("pipeline foundation", () => { } }) + it("reports adjacent pipeline step compatibility hints", () => { + const recipe = buildRecipe({ + steps: [ + { + id: "encode", + toolKey: "base64_encode_decode", + adapterVersion: 1, + inputMode: "previous_output", + options: { operation: "encode" }, + }, + { + id: "format", + toolKey: "json_formatter", + adapterVersion: 1, + inputMode: "previous_output", + options: { mode: "pretty", indent: 2 }, + }, + { + id: "constant_json", + toolKey: "json_formatter", + adapterVersion: 1, + inputMode: "constant", + constantInput: "{\"ok\":true}", + options: { mode: "minify" }, + }, + ], + edges: [], + }) + + expect(getStepCompatibilityHints(recipe.steps)).toEqual([ + { + fromKind: "text", + fromStepId: "encode", + toKind: "json", + toStepId: "format", + }, + ]) + }) + it("round trips recipe URL encoding", () => { const recipe = buildRecipe() const encoded = encodeRecipeForUrl(recipe) diff --git a/tests/unit/regex-tester-utils.test.ts b/tests/unit/regex-tester-utils.test.ts new file mode 100644 index 00000000..3fc7c43f --- /dev/null +++ b/tests/unit/regex-tester-utils.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest" +import { testRegexPattern } from "@/features/tools/regex-tester/utils" + +describe("regex tester utils", () => { + it("returns match summaries with captures", () => { + const result = testRegexPattern("([A-Z][a-z]+)(\\d)", "g", "Ab1 Cd2") + + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.matches).toEqual([ + { match: "Ab1", index: 0, groupIndex: 0, groups: ["Ab", "1"] }, + { match: "Cd2", index: 4, groupIndex: 1, groups: ["Cd", "2"] }, + ]) + } + }) + + it("returns structured invalid regex errors", () => { + const result = testRegexPattern("[", "g", "input") + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error).toMatch(/Invalid regular expression/) + } + }) +}) diff --git a/tests/unit/saml-decoder-utils.test.ts b/tests/unit/saml-decoder-utils.test.ts new file mode 100644 index 00000000..9c7a4aa2 --- /dev/null +++ b/tests/unit/saml-decoder-utils.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest" +import { PHASE4_LIMITS } from "@/core/utils/phase4-inspector-limits" +import { decodeSaml } from "@/features/tools/saml-decoder/utils" + +const SAMPLE_SAML = `