mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Feature/#126 (#193)
* fix(toss-securities): clarify session expiry and quote 403 handling * Clarify toss empty-output session expiry Portfolio and watchlist reads can exit successfully with empty payloads when the stored Toss session has expired. The empty-output path now verifies the session before JSON parsing and only promotes confirmed invalid auth doctor data into TossSessionExpiredError. Constraint: Scope is limited to toss-securities issue #126 follow-up on PR #192 Rejected: Treat auth doctor execution failures as expired sessions | unsupported or failing doctor output is inconclusive without parsed session.valid=false Confidence: high Scope-risk: narrow Directive: Keep empty-result session expiry classification tied to explicit auth doctor confirmation Tested: npm run test --workspace toss-securities; npm run lint --workspace toss-securities; npm run ci; manual mock tossctl blank stdout invalid/inconclusive doctor checks * Avoid false session-expiry labels for validation errors The toss wrapper now treats bare validation_error text as an upstream command failure instead of a session-expired signal. Structured auth doctor JSON remains the source of truth for empty portfolio/watchlist invalid-session promotion, while known stored-session-invalid stderr still maps to TossSessionExpiredError.\n\nConstraint: PR #192 follow-up must stay scoped to issue #126 toss-securities behavior.\nRejected: Keep validation_error in the global regex | it mislabels auth doctor transport failures and quote 403 validation errors as session expiry.\nConfidence: high\nScope-risk: narrow\nDirective: Do not broaden the free-text session classifier without regressions for auth doctor and quote upstream validation failures.\nTested: npm run lint --workspace toss-securities; npm run test --workspace toss-securities; npm run ci; manual mock tossctl validation_error checks; architect verification CLEAR\nNot-tested: Live tossctl network/auth session against real Toss upstream * Preserve toss empty-response auth-doctor contract The prior review identified the empty portfolio/watchlist promotion rule as an upstream-contract dependency worth making explicit. Add regression coverage for the non-invalid auth doctor path and document that only parsed JSON with session.valid false promotes empty results to TossSessionExpiredError. Constraint: Scope is issue #126 / toss-securities only; public-restroom-nearby changes are excluded. Rejected: Treat any auth doctor output as session-expiry evidence | false positives would relabel valid empty portfolio/watchlist responses. Confidence: high Scope-risk: narrow Directive: Do not broaden empty-response promotion unless tossctl provides a stronger authenticated-empty-result contract. Tested: npm run lint --workspace toss-securities Tested: npm run test --workspace toss-securities (15/15) Tested: npm run ci Tested: Manual mock tossctl empty portfolio with session.valid true preserved [] Tested: Architect verification CLEAR Not-tested: Live Toss Securities account session behavior. --------- Co-authored-by: galvaomica <galvaomica@galvaomicaui-MacBookAir.local>
This commit is contained in:
parent
69637b3a46
commit
3cea4be6eb
5 changed files with 403 additions and 8 deletions
11
.changeset/toss-session-expiry-clarity.md
Normal file
11
.changeset/toss-session-expiry-clarity.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
"toss-securities": minor
|
||||
---
|
||||
|
||||
Improve toss-securities session-expiry handling and diagnostics.
|
||||
|
||||
- Add `auth doctor` wiring and `checkSession()` helper.
|
||||
- Add `TossSessionExpiredError` for clearer invalid-session failures.
|
||||
- Promote silent empty-array responses from portfolio/watchlist into explicit session-expired errors when `auth doctor` says session is invalid.
|
||||
- Add `search/stocks 403` upstream hinting for quote failures.
|
||||
- Extend tests and README to document behavior and `tossctl >= 0.3.6` recommendation.
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
먼저 upstream CLI 를 설치합니다.
|
||||
|
||||
중요: `tossctl >= 0.3.6` 사용을 권장합니다. (`quote` 403 / 세션 관련 upstream 이슈 #15 반영 버전)
|
||||
|
||||
```bash
|
||||
brew tap JungHoonGhae/tossinvest-cli
|
||||
brew install tossctl
|
||||
|
|
@ -31,9 +33,16 @@ npm install toss-securities
|
|||
- `listOrders()`
|
||||
- `listCompletedOrders({ market })`
|
||||
- `listWatchlist()`
|
||||
- `checkSession()`
|
||||
|
||||
모든 helper 는 내부적으로 `tossctl ... --output json` 을 실행하고, `commandName`, `bin`, `args`, `data` 를 반환합니다.
|
||||
|
||||
세션 만료 관련:
|
||||
- `account summary` 등은 만료 시 에러를 던집니다.
|
||||
- 일부 커맨드(`portfolio positions`, `watchlist list`)는 upstream에서 빈 배열(`[]`)을 반환할 수 있어, 이 패키지는 기본적으로 `auth doctor`를 추가 확인해 만료를 `TossSessionExpiredError`로 승격합니다.
|
||||
- 이 승격은 `auth doctor`가 파싱 가능한 JSON을 반환하고 `session.valid === false`로 명시 확인될 때만 발생합니다. `auth doctor` 실패, 파싱 불가 출력, 또는 `session.valid !== false`는 세션 만료 판정으로 취급하지 않습니다.
|
||||
- 필요하면 `verifySessionOnEmpty: false`로 기존 빈 배열 동작을 유지할 수 있습니다.
|
||||
|
||||
대응되는 대표 CLI 는 `tossctl account summary --output json`, `tossctl quote get TSLA --output json`, `tossctl watchlist list --output json` 입니다.
|
||||
|
||||
## Usage
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ const {
|
|||
} = require("./parse");
|
||||
|
||||
const execFile = util.promisify(childProcess.execFile);
|
||||
const SESSION_EXPIRED_PATTERN = /stored session is no longer valid/iu;
|
||||
|
||||
class TossSessionExpiredError extends Error {
|
||||
constructor(message, details = {}) {
|
||||
super(message, { cause: details.cause });
|
||||
this.name = "TossSessionExpiredError";
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function buildReadOnlyCommand(commandName, options = {}) {
|
||||
return {
|
||||
|
|
@ -15,6 +24,68 @@ function buildReadOnlyCommand(commandName, options = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function shouldVerifySessionOnEmpty(commandName, options = {}) {
|
||||
if (options.verifySessionOnEmpty === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return commandName === "portfolioPositions" || commandName === "watchlistList";
|
||||
}
|
||||
|
||||
function isConfirmedInvalidSession(doctor) {
|
||||
return doctor?.data?.session?.valid === false;
|
||||
}
|
||||
|
||||
function buildSessionExpiredError(commandName, result, doctor, emptyKind) {
|
||||
return new TossSessionExpiredError(
|
||||
`tossctl ${commandName} returned ${emptyKind} while session is invalid. Run \`tossctl auth login\`.`,
|
||||
{
|
||||
commandName,
|
||||
stderr: String(result.stderr || "").trim(),
|
||||
doctor: doctor.data
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function enrichQuote403Message(commandName, detail) {
|
||||
const text = String(detail || "");
|
||||
const isQuote = commandName === "quoteGet" || commandName === "quoteBatch";
|
||||
|
||||
if (!isQuote) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (/search\/stocks/iu.test(text) && /403/.test(text)) {
|
||||
return `${text} | Upstream hint: if this recurs, report to https://github.com/JungHoonGhae/tossinvest-cli/issues/15 with timestamp and symbol.`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function checkSession(options = {}) {
|
||||
const result = await runReadOnlyCommand("authDoctor", {
|
||||
...options,
|
||||
verifySessionOnEmpty: false
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getConfirmedInvalidSession(options = {}) {
|
||||
try {
|
||||
const doctor = await checkSession(options);
|
||||
if (isConfirmedInvalidSession(doctor)) {
|
||||
return doctor;
|
||||
}
|
||||
} catch {
|
||||
// Treat doctor execution/parsing failures as inconclusive. Empty portfolio
|
||||
// and watchlist responses should only become TossSessionExpiredError when
|
||||
// auth doctor returns parsed data that explicitly confirms invalid session.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runReadOnlyCommand(commandName, options = {}) {
|
||||
const command = buildReadOnlyCommand(commandName, options);
|
||||
|
||||
|
|
@ -26,14 +97,42 @@ async function runReadOnlyCommand(commandName, options = {}) {
|
|||
maxBuffer: options.maxBuffer || 1024 * 1024
|
||||
});
|
||||
|
||||
if (shouldVerifySessionOnEmpty(commandName, options) && !String(result.stdout || "").trim()) {
|
||||
const doctor = await getConfirmedInvalidSession(options);
|
||||
if (doctor) {
|
||||
throw buildSessionExpiredError(commandName, result, doctor, "empty output");
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseJsonOutput(result.stdout, commandName);
|
||||
|
||||
if (shouldVerifySessionOnEmpty(commandName, options) && Array.isArray(parsed.data) && parsed.data.length === 0) {
|
||||
const doctor = await getConfirmedInvalidSession(options);
|
||||
if (doctor) {
|
||||
throw buildSessionExpiredError(commandName, result, doctor, "empty array");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...command,
|
||||
...parseJsonOutput(result.stdout, commandName),
|
||||
...parsed,
|
||||
stderr: result.stderr
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TossSessionExpiredError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const stderr = String(error.stderr || "").trim();
|
||||
const detail = stderr || error.message;
|
||||
const detail = enrichQuote403Message(commandName, stderr || error.message);
|
||||
|
||||
if (SESSION_EXPIRED_PATTERN.test(detail)) {
|
||||
throw new TossSessionExpiredError(`tossctl ${commandName} failed: ${detail}`, {
|
||||
commandName,
|
||||
stderr,
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`tossctl ${commandName} failed: ${detail}`, {
|
||||
cause: error
|
||||
|
|
@ -85,6 +184,7 @@ function getQuoteBatch(symbols, options = {}) {
|
|||
|
||||
module.exports = {
|
||||
buildReadOnlyCommand,
|
||||
checkSession,
|
||||
getAccountSummary,
|
||||
getPortfolioAllocation,
|
||||
getPortfolioPositions,
|
||||
|
|
@ -94,5 +194,6 @@ module.exports = {
|
|||
listCompletedOrders,
|
||||
listOrders,
|
||||
listWatchlist,
|
||||
runReadOnlyCommand
|
||||
runReadOnlyCommand,
|
||||
TossSessionExpiredError
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ const READ_ONLY_COMMANDS = Object.freeze({
|
|||
buildExtraArgs(options = {}) {
|
||||
return normalizeSymbols(options.symbols);
|
||||
}
|
||||
},
|
||||
authDoctor: {
|
||||
segments: ["auth", "doctor"]
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ const path = require("node:path");
|
|||
|
||||
const {
|
||||
buildReadOnlyCommand,
|
||||
checkSession,
|
||||
getAccountSummary,
|
||||
getQuote
|
||||
getPortfolioPositions,
|
||||
getQuote,
|
||||
listWatchlist,
|
||||
TossSessionExpiredError
|
||||
} = require("../src/index");
|
||||
const {
|
||||
assertReadOnlyCommandName,
|
||||
|
|
@ -70,16 +74,16 @@ test("public helpers execute a mock tossctl binary and parse its JSON output", a
|
|||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
printf '%s\n' "$@" > "${logFile}"
|
||||
printf '%s\\n' "$@" > "${logFile}"
|
||||
if [ "$7" = "account" ] && [ "$8" = "summary" ]; then
|
||||
printf '{"accountNo":"123-45","totalAssetAmount":1500000}\n'
|
||||
printf '{"accountNo":"123-45","totalAssetAmount":1500000}\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "quote" ] && [ "$4" = "get" ]; then
|
||||
printf '{"symbol":"%s","price":123.45}\n' "$5"
|
||||
printf '{"symbol":"%s","price":123.45}\\n' "$5"
|
||||
exit 0
|
||||
fi
|
||||
printf '{"args":"%s"}\n' "$*"
|
||||
printf '{"args":"%s"}\\n' "$*"
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
|
|
@ -101,3 +105,270 @@ printf '{"args":"%s"}\n' "$*"
|
|||
assert.equal(quote.data.symbol, "005930");
|
||||
assert.match(fs.readFileSync(logFile, "utf8"), /quote|get/);
|
||||
});
|
||||
|
||||
test("account summary invalid session throws TossSessionExpiredError", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-expire-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "account" ] && [ "$4" = "summary" ]; then
|
||||
echo "Error: stored session is no longer valid; run \\\`tossctl auth login\\\`" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
getAccountSummary({ env }),
|
||||
(error) => error instanceof TossSessionExpiredError && /stored session is no longer valid/.test(error.message)
|
||||
);
|
||||
});
|
||||
|
||||
test("portfolio empty array with invalid auth doctor is promoted to TossSessionExpiredError", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-empty-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "portfolio" ] && [ "$4" = "positions" ]; then
|
||||
printf '[]\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
printf '{"session":{"valid":false,"validation_error":"401"}}\\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(getPortfolioPositions({ env }), TossSessionExpiredError);
|
||||
const passthrough = await getPortfolioPositions({ env, verifySessionOnEmpty: false });
|
||||
assert.deepEqual(passthrough.data, []);
|
||||
});
|
||||
|
||||
test("portfolio empty array is preserved when auth doctor does not confirm invalid session", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-empty-valid-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "portfolio" ] && [ "$4" = "positions" ]; then
|
||||
printf '[]\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
printf '{"session":{"valid":true}}\\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
const result = await getPortfolioPositions({ env });
|
||||
|
||||
assert.deepEqual(result.data, []);
|
||||
});
|
||||
|
||||
test("portfolio blank stdout with invalid auth doctor is promoted to TossSessionExpiredError", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-blank-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "portfolio" ] && [ "$4" = "positions" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
printf '{"session":{"valid":false,"validation_error":"401"}}\\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
getPortfolioPositions({ env }),
|
||||
(error) =>
|
||||
error instanceof TossSessionExpiredError &&
|
||||
/returned empty output while session is invalid/.test(error.message)
|
||||
);
|
||||
await assert.rejects(
|
||||
getPortfolioPositions({ env, verifySessionOnEmpty: false }),
|
||||
/returned empty output/
|
||||
);
|
||||
});
|
||||
|
||||
test("portfolio blank stdout keeps auth doctor failures inconclusive", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-doctor-fail-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "portfolio" ] && [ "$4" = "positions" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
echo "validation_error: transport failure" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
getPortfolioPositions({ env }),
|
||||
(error) =>
|
||||
!(error instanceof TossSessionExpiredError) &&
|
||||
/returned empty output/.test(error.message)
|
||||
);
|
||||
});
|
||||
|
||||
test("watchlist empty array with invalid auth doctor is promoted to TossSessionExpiredError", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-watchlist-empty-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "watchlist" ] && [ "$4" = "list" ]; then
|
||||
printf '[]\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
printf '{"session":{"valid":false,"validation_error":"401"}}\\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(listWatchlist({ env }), TossSessionExpiredError);
|
||||
const passthrough = await listWatchlist({ env, verifySessionOnEmpty: false });
|
||||
assert.deepEqual(passthrough.data, []);
|
||||
});
|
||||
|
||||
test("watchlist empty array is preserved when auth doctor does not confirm invalid session", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-watchlist-empty-valid-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "watchlist" ] && [ "$4" = "list" ]; then
|
||||
printf '[]\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
printf '{"session":{"valid":true}}\\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '{"ok":true}\\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
const result = await listWatchlist({ env });
|
||||
|
||||
assert.deepEqual(result.data, []);
|
||||
});
|
||||
|
||||
test("quote 403 includes upstream hint", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-403-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
echo "status 403 at search/stocks" 1>&2
|
||||
exit 1
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
getQuote("ALM", { env }),
|
||||
(error) =>
|
||||
!(error instanceof TossSessionExpiredError) &&
|
||||
/issues\/15/.test(error.message)
|
||||
);
|
||||
});
|
||||
|
||||
test("checkSession treats auth doctor validation_error failures as inconclusive command errors", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-check-session-fail-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
|
||||
echo "validation_error: transport failure" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
printf '{"ok":true}\n'
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
checkSession({ env }),
|
||||
(error) =>
|
||||
!(error instanceof TossSessionExpiredError) &&
|
||||
/validation_error: transport failure/.test(error.message)
|
||||
);
|
||||
});
|
||||
|
||||
test("quote search stocks 403 with validation_error remains a non-session upstream error", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-403-validation-error-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
echo "validation_error: status 403 at search/stocks" 1>&2
|
||||
exit 1
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
||||
|
||||
await assert.rejects(
|
||||
getQuote("ALM", { env }),
|
||||
(error) =>
|
||||
!(error instanceof TossSessionExpiredError) &&
|
||||
/validation_error: status 403 at search\/stocks/.test(error.message) &&
|
||||
/issues\/15/.test(error.message)
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue