mirror of
https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
synced 2026-09-29 16:57:48 +08:00
feat(dingtalk-tag): 数字员工管理、身份登录与本地 Agent 接入 (#1391)
* feat(dev): add DEAP agent commands
* feat(dev): add DEAP profile flags
* feat(deap-agent): 观测命令对齐 HSF 接口收敛
- 删 run-executions、resolve-run-id 两个命令(HSF 接口已移除对应方法)
- run-status 改双定位:--run-id 或 --source-id+--source-type 二选一,全场域通用;
移除旧的 assistantId+taskId 单聊口径
- 新增 send-message:以数字员工身份发消息返回 runId,写操作需 --yes 确认
- source-type 加白名单 enum(im_message/trigger_rule/scenario_instance)
- 同步契约测试与叶子清单
* refactor(deap-agent): source-type 白名单收敛为 im_message + trigger_rule
chat-2 删除 scenario_instance 来源类型后同步;并注明 im_message 的 sourceId 是
钉钉开放态 openMessageId,单聊/群@/群感知三条链路含义一致。
* fix(deap-agent): 按规范解析内建 MCP 端点
* fix: align deap agent cli with mcp tools
* refactor(deap-agent): 删除 send-message;run-status/trace 只按来源定位
- 删除 send-message 命令(HSF 侧 sendMessage 已移除)
- run-status / trace 去掉 --run-id 与 --trace-id,统一 --source-id + --source-type
且与 --assistant-id 一并必填:调用方拿不到 runId,留个填不了的 flag 只诱导瞎传
- 保留辰驷 c158ea17 对生命周期命令的 MCP 文案对齐
* refactor(deap): 提到顶级 dws deap 前缀,分管理态/观测态两子组
DEAP 是独立平台,与开放平台应用(agentId/clientId/机器人配置)无包含关系,
此前挂在 dws dev deap-agent 下会让两个产品的路由与文案互相干扰。
命令树:
- dws deap manage create/detail/list/save-draft/publish/delete(含不可逆写)
- dws deap observe run-status/trace(全部只读)
契约:ProductID dev→deap,canonicalPath 与 CLIPath 全量更新;MCP tool 名不变,
MCP 侧配置无需调整。
Skill 文档:新增 references/deap/(index + manage + observe),并在 misc 路由表、
说明与 frontmatter 注册。observe.md 写清 openTaskId→openMessageId 两跳取法——
dws chat message send 只返回 openTaskId,直接拿它查必然 NOT_FOUND,而报错
看不出是 ID 类型错了。
* refactor(deap): dev.go / devapp_test.go 回退至主干,不在其领域留痕
dev 是开放平台应用的命令树,属另一领域。DEAP 迁到顶级 dws deap 后,
dev 侧无需任何改动——之前加的挂载已删,剩下的注释与文案提示也一并回退,
两个文件现与 origin/main 逐字一致。
DEAP 不再挂 dev 的事实由 deap_agent_test.go 的 TestDeapIsNotMountedUnderDev
单独钉住(断言写在本领域测试文件里,不侵入 dev 的测试)。
* docs(deap): 去掉与 dev 的对比性提示
那些"两者是不同产品/别搞混/曾挂在 dev 下"的措辞源于早先把 DEAP 挂在 dev 下的
误解。dev 与 DEAP 本无关联,写对比反而凭空建立联想并暗示一段已不存在的历史。
清理:deap-index.md 的混淆警示、misc SKILL.md 的路由提示、deapHandler 与
deapProductID 的注释、TestDeapIsNotMountedUnderDev(该测试同时耦合 devHandler)。
保留 AvoidWhen 里的产品边界声明:create 在两侧同名,属真实可能的误选,
与"别搞混"的元叙述不同。
* feat(deap): select detail configuration type
* ci(actions): 简化构建并发布 Release
* feat: add Skill and MCP commands to deap agent
* fix(deap): compose Skill upload and creation
* fix(deap): route MCP query to published tool
* fix(deap): route skill query to registered tool
* feat(deap): support combined response modes
* feat(deap): use scoped credential for skill upload
* fix(deap): call published skill upload credential tool
* fix(deap): follow renamed upload credential tool
* docs(deap): explain temporary dws token workflow
* docs(deap): remove deprecated dws token workflow
* feat(dingtalk-tag): rename deap command and add auth token
* docs(dingtalk-tag): clarify dws auth credential response
* refactor(dingtalk-tag): group capabilities and unify agent IDs
* feat(dingtalk-tag): support main program type
* docs(dingtalk-tag): document local agent program type
* feat(skill): align dingtalk-tag guidance
* feat(dingtalk-tag): connect digital employees to local DSH
* fix(dingtalk-tag): constrain main program types
* feat(dingtalk-tag): filter employee list by program type
* fix(dingtalk-tag): resolve managed token identity
* fix(dingtalk-tag): bind managed identities to published profile
* fix(dingtalk-tag): resolve async reply receipts
* docs(dingtalk-tag): clarify default program type guidance
* feat(dingtalk-tag): add profile-only connect mode [skip ci]
* fix(dingtalk-tag): resolve operator in employee token scope
* feat(dingtalk-tag): add isolated local Agent adapters
* fix(dingtalk-tag): close registration races and verify worker lifecycle
* fix(dingtalk-tag): validate saved employee self identity
* feat(dingtalk-tag): unify adapter lifecycle and DSH release control
* fix(dingtalk-tag): fence lost hosts and recover DSH registration
* feat(dingtalk-tag): rename managed login command
* feat(dingtalk-tag): complete managed employee login
* fix(auth): complete external code login and address review findings
Based on build.17.1. Expose auth exchange with client-id selection, verified identity persistence, transactional direct credentials, scoped configuration and accurate provenance. Redact code arguments in performance reports. Include regression tests and bounded real-service verification notes.
* fix(auth): isolate exchange invocations and preserve credential pairing
* fix(auth): close early-exit and config-snapshot isolation gaps
* fix(dingtalk-tag): tolerate temporarily unavailable send receipts
* feat(dingtalk-tag): integrate server device binding lifecycle
* fix(dingtalk-tag): fence uncertain binding writes and preserve retry receipts
* test(dingtalk-tag): update binding command and interface contracts
* fix: refresh local agent binding token and classify rejections
* chore: repair digital employee release fragment metadata
* fix: align local agent MCP tool names
* fix(release): upload only distributable archives and checksums
* docs(release): require change notes and include installation guidance
* fix: align local agent MCP binding contract (#13)
Validated against the pre-production bind -> rebind -> unbind lifecycle on PR head 49e20e79cdbf9b07efbc581aefe90cb839957dff.
* fix(dingtalk-tag): mark digital employee event source
* feat(dingtalk-tag): remove connect profile-only mode
* feat(dingtalk-tag): 移除 connect bind 和 rebind 子命令
* fix(dingtalk-tag): flatten MCP config and require employee scope
* fix(connect): 修正未知命令纠错和发布详情错误提示
* docs(dingtalk-tag): align MCP draft auto-mount guidance
* docs(dingtalk-tag): correct MCP auto-mount release note
* feat(dingtalk-tag): add digital employee management and local agent integration
* fix(dingtalk-tag): align dry-run contracts and cross-platform CI
* fix(event): 修正数字员工传输就绪与系统帧处理
* docs: 补充数字员工真实收发与恢复验收证据
* test(dingtalk-tag): cover platform identity and failure boundaries
* test(dingtalk-tag): exercise worker startup and upload failure paths
* fix(dingtalk-tag): handle Windows shutdown and corrupt state paths
* test(dingtalk-tag): cover login delivery and unbind recovery boundaries
* docs: 明确原消息故障未解决并补充来源对照
* fix: adapt upstream named registration and test employee failure boundaries
* fix: complete employee lifecycle cancellation and failure coverage
* test: wait for consumer startup before cancellation
* docs: align shared skill descriptions with upstream routing
* fix: allow atomic employee state replacement during Windows reads
* fix: retry bounded Windows snapshot rename contention
* test: exercise employee daemon lifecycle on Windows
* feat(dingtalk-tag): align user-facing manage contract
* fix(dingtalk-tag): unify supervisor user id field
* fix: 数字员工事件消费沿用默认来源
* fix(dingtalk-tag): unify avatar URL handling
* fix(dingtalk-tag): map agent type to MCP contract
* fix(dingtalk-tag): validate response mode by agent type
* feat(dingtalk-tag): add manage set-visibility command and align merged tests
- Add `dws dingtalk-tag manage set-visibility` leaf binding the set_visibility
MCP tool (agent-uuid/visibility required; staff-ids/dept-ids as full-replace
string slices), with full contract (Identity/Interface/Safety/Selection).
- Align merge-carried PR-only tests with the feature branch's authoritative
contract:
- drop deapAgentProfileJSON assertions (profile-json flag removed by feature)
- remove retired employee-no size-validation case
- update published-detail missing-identity error assertion to the generalized
message that no longer leaks field names
- provide required --response-mode in RemovesRetiredFlags create case (open_code
default now mandates it) and reflect it in expected MCP args
* fix: address PR #1391 CI lint and auto-CR P1/P2 findings
- CI Lint (make format-check): gofmt-realign the create-args map literal in
deap_agent_test.go so the format gate passes and the full test matrix runs.
- [P1] external_exchange.go: reuse the ClientSecret of the finally selected
clientID by scanning all candidates. The reuse loop was gated on
clientID == configured.id, but configured is only the first valid candidate,
so explicitly selecting a later app/env candidate via --client-id dropped its
unmanaged secret and fell through to an empty-secret managed exchange,
breaking that app's normal OAuth authorization-code login. Managed-ness is now
decided per candidate. Adds env/app later-candidate regression tests.
- [P2] apiclient: reject CR/LF in the streaming UploadMultipart field keys,
file field name and file name before creating the pipe/goroutine, matching the
existing non-streaming newMultipartBody guard, so user-controlled names cannot
inject extra MIME headers/fields. Adds streaming-entry injection tests.
* test(app): align deap-agent schema contract with simplified dingtalk-tag contract
The feature branch simplified the digital-employee create/save-draft/list
contract in source but internal/app/schema_deap_agent_contract_test.go still
asserted the pre-simplification shape, so the final-schema gate failed once CI
Lint stopped fail-fast-skipping the matrix. Verified the same failures exist on
the pristine feature branch (pre-existing test debt, not a merge artifact).
- create_digital_employee: 11 -> 7 params (drop dept-name/icon/profile-json/
employee-no/position-name/supervisor-uid; add avatar-url/supervisor-user-id;
main-program-type property digitalTagEmployeeProfile.mainProgramType -> type).
- update_digital_employee_draft: 15 -> 11 params (same removals/additions).
- list_digital_employees: main-program-type property mainProgramType -> type.
- MCP auto-mount help/schema: expected phrases 自动追加/selectedSkills ->
自动挂载/回读确认 to match the current create_mcp guidance.
- Add set_visibility to the final-schema contract (new manage leaf: write/high/
user_required; agent-uuid/visibility required, staff-ids/dept-ids arrays).
* fix(dingtalk-tag): make create example pass agent dry-run gate
The create_digital_employee example was rewritten to showcase --avatar-url but
dropped --response-mode and pointed avatar-url at a local ./avatar.png. Under
DWS_AGENT_EXAMPLES_DRY_RUN the example must survive the command's own
validation: open_code (default) now requires a --response-mode, and a local
avatar path must be a readable image file, which the dry-run harness does not
materialize. Restore --response-mode mention_only and use a public HTTPS avatar
URL so the example dry-runs through the standard request preview without a
local file read.
* test(helpers): reach 100% changed-code coverage for deap avatar/skill-upload; preserve credential error cause
The changed-code coverage gate (target 100%) failed at 97.74% because the
platform harness only runs TestCrossPlatformCoverage*/TestAllShortcuts tests,
and the digital-employee avatar-upload feature shipped without such tests.
- Rename the four TestDevDeapAgent* avatar/local-agent tests to the
TestCrossPlatformCoverage* convention so the platform coverage harness runs
them (avatar uncovered statements 59 -> 32).
- Add deap_agent_avatar_coverage_test.go covering the remaining branches:
avatar URL validation (http-no-host/unsafe-path/bad-ext/unreadable/dir/
oversize/valid), stage-error formatting + Unwrap, created-UUID envelope
traversal, profile preparation, invalid main-program-type, save-draft
response-mode/avatar validation, create/save avatar call branches (dry-run,
transport error, parse error, upload error), and the OpenAPI avatar uploader
delegate.
- Improve the skill-upload credential error: stop collapsing every cause into a
bare "OpenAPI 认证失败". Preserve the underlying stage cause and server
code/trace for the MCP credential call (no secret exists yet on failure),
while still redacting the injected resolver error so credential material can
never leak. Covered by a new temporaryCredential MCP-branch test.
Local platform coverage gate now reports 100.0000% (2971 changed statements).
* test(helpers): make avatar unsafe-path case cross-platform for Windows coverage
The avatar URL-input branch test used "/abs/path.png" to trigger SafeInputPath's
absolute-path rejection, but filepath.IsAbs("/abs/path.png") is false on Windows,
so it fell through to the unreadable-file branch and failed Coverage (Windows)
with '本地文件不可读' instead of '路径不安全'. Use a NUL control character
("invalid\x00.png"), which rejectControlChars rejects on every platform (same
convention as the existing upload-boundaries test), keeping the unsafe-path
branch covered cross-platform.
* fix(dingtalk-tag): gate employee enqueue on capacity before writing ledger
Previously enqueue wrote the audit entry and dedup task file before checking
the 128-conversation and 32-per-queue capacity limits. A transient capacity
rejection therefore left an 'accepted' task file behind, so Event Bus
redelivery of the same event was short-circuited as already handled and the
message was permanently swallowed (recovery only rewrites accepted->needs_review,
never reprocessing).
Move both capacity checks ahead of audit/writeEmployeeJSON. enqueue holds r.mu
and is the sole producer, so a queue with room can never fill before the send,
letting the record persist only once acceptance is guaranteed; the send is now
unconditional and non-blocking.
Add regression coverage asserting that queue_capacity and conversation_capacity
rejections leave no dedup record and that redelivering the same event after the
backpressure clears is accepted and persisted.
* docs(dingtalk-tag): describe set-visibility scope as ALL/PARTIAL
Align the set-visibility command help, flag usage, MCP description, example and
parameter docs with the server contract that now accepts ALL and PARTIAL
(SELECTED kept only as a legacy alias). PARTIAL is stated explicitly to mean
'visible to the specified members/departments', and the example uses the full
--visibility PARTIAL value instead of the truncated PART.
* feat(dingtalk-tag): unify capability lifecycle
* 补齐数字员工能力跨平台覆盖测试
* docs: add reproducible test evidence for PR #1391
* fix(dingtalk-tag): upload the validated skill file handle
* docs: refresh PR 1391 test evidence and CR resolutions
* ci: give Windows native coverage enough time to finish
* fix(dingtalk-tag): align profile contracts and require explicit type
* fix(test): satisfy required employee type fixture
---------
Co-authored-by: chensi.wx <chensi.wx@alibaba-inc.com>
Co-authored-by: siyideng <dsy274007@alibaba-inc.com>
Co-authored-by: chichuan <haofeng.hf@alibaba-inc.com>
Co-authored-by: 赤川 <30925823+haofeng0705@users.noreply.github.com>
Co-authored-by: 丞天 <yuanchenyu.ycy@alibaba-inc.com>
Co-authored-by: dingpan <dingpan.dp@alibaba-inc.com>
This commit is contained in:
co-authored by
chensi.wx
siyideng
chichuan
赤川
丞天
dingpan
parent
002121d7bd
commit
42d8178a5a
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise a packaged CLI against localhost OAuth/MCP fixtures, without real credentials."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", required=True)
|
||||
args = parser.parse_args()
|
||||
command = [str(Path(args.binary).resolve())]
|
||||
calls = []
|
||||
errors = []
|
||||
state = {"app": "official-app", "bad_identity": False}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def respond(self, value):
|
||||
data = json.dumps(value).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
calls.append("discover")
|
||||
if self.path != "/cli/clientId":
|
||||
errors.append("unexpected discovery path")
|
||||
self.respond({"success": True, "result": "official-app"})
|
||||
|
||||
def do_POST(self):
|
||||
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
if self.path in ("/oauth2/getToken", "/oauth2/refreshToken"):
|
||||
refresh = self.path.endswith("refreshToken")
|
||||
calls.append("refresh" if refresh else "exchange")
|
||||
if body.get("clientId") != state["app"] or "clientSecret" in body:
|
||||
errors.append("incorrect application/credential pairing")
|
||||
if not refresh and body.get("authCode") != "fixture-code":
|
||||
errors.append("incorrect code input")
|
||||
if refresh and body.get("refreshToken") != "fixture-refresh":
|
||||
errors.append("incorrect refresh token")
|
||||
self.respond({"accessToken": "fixture-access", "refreshToken": "fixture-refresh",
|
||||
"expiresIn": 7200 if refresh else 1, "corpId": "fixture-corp"})
|
||||
return
|
||||
method = body.get("method")
|
||||
if method == "tools/call":
|
||||
calls.append("identity")
|
||||
if body.get("params", {}).get("name") != "get_current_user_profile":
|
||||
errors.append("unexpected identity tool")
|
||||
if self.headers.get("x-user-access-token") != "fixture-access":
|
||||
errors.append("identity lookup did not use exchanged token")
|
||||
identity = {"corpId": "fixture-corp", "userId": "fixture-user"}
|
||||
if state["bad_identity"]:
|
||||
identity.pop("userId")
|
||||
result = {"content": [{"type": "text", "text": json.dumps({
|
||||
"result": [{"orgEmployeeModel": identity}]})}], "isError": False}
|
||||
elif method == "initialize":
|
||||
result = {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": "exchange-fixture", "version": "1"}}
|
||||
else:
|
||||
errors.append("unexpected MCP request")
|
||||
result = {}
|
||||
self.respond({"jsonrpc": "2.0", "id": body.get("id"), "result": result})
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
url = f"http://127.0.0.1:{server.server_port}"
|
||||
checks = []
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="dws-exchange-smoke-") as tmp:
|
||||
def environment(name):
|
||||
directory = Path(tmp) / name
|
||||
directory.mkdir()
|
||||
(directory / "mcp_url").write_text(url)
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if not k.startswith(("DWS_", "DINGTALK_"))}
|
||||
env.update(DWS_CONFIG_DIR=str(directory), DWS_KEYCHAIN_DIR=str(directory / "keys"),
|
||||
DWS_DISABLE_KEYCHAIN="1", DINGTALK_CONTACT_MCP_URL=url + "/contact",
|
||||
DWS_ALLOW_HTTP_ENDPOINTS="1", DWS_TRUSTED_DOMAINS="127.0.0.1")
|
||||
return env, directory
|
||||
|
||||
def run(argv, env, stdin="", success=True):
|
||||
report = Path(env["DWS_CONFIG_DIR"]) / "perf.json"
|
||||
report.unlink(missing_ok=True)
|
||||
env = dict(env, DWS_PERF_REPORT=str(report))
|
||||
result = subprocess.run(command + argv + ["--format", "json"], env=env,
|
||||
input=stdin, capture_output=True, text=True, timeout=90)
|
||||
if (result.returncode == 0) != success:
|
||||
# Fixtures only; keep even their token strings out of reports.
|
||||
detail = result.stderr
|
||||
for secret in ("fixture-code", "fixture-access", "fixture-refresh"):
|
||||
detail = detail.replace(secret, "[redacted]")
|
||||
raise AssertionError(f"unexpected CLI exit {result.returncode}: {detail[:1200]}")
|
||||
surfaces = result.stdout + result.stderr
|
||||
if argv[:2] == ["auth", "exchange"] and "--mcp" not in argv:
|
||||
assert report.exists(), "exchange did not write a performance report for verification"
|
||||
if report.exists():
|
||||
surfaces += report.read_text()
|
||||
assert "command" in json.loads(report.read_text()), "missing performance command field"
|
||||
for secret in ("fixture-code", "fixture-access", "fixture-refresh"):
|
||||
assert secret not in surfaces, "credential in stdout/stderr/performance report"
|
||||
return json.loads(result.stdout) if success else None
|
||||
|
||||
for name, selection in (("default", ["--client-id", "default"]),
|
||||
("custom", ["--client-id", "custom-app"]), ("omitted", [])):
|
||||
env, directory = environment(name)
|
||||
state["app"] = "custom-app" if name == "custom" else "official-app"
|
||||
before = len(calls)
|
||||
login = run(["auth", "exchange", "--code-stdin"] + selection, env, "fixture-code\n")
|
||||
assert login["data"]["dwsProfile"] == "fixture-corp:fixture-user"
|
||||
assert login["data"]["clientId"] == state["app"]
|
||||
assert login["data"]["isCurrentProfile"]
|
||||
# Short-lived fixture triggers an actual refresh in another CLI process.
|
||||
status = run(["auth", "status"], env)
|
||||
assert status.get("user_id") == "fixture-user", "status missing employee userId"
|
||||
assert status.get("authenticated") and status.get("token_valid") and status.get("refreshed"), \
|
||||
"status did not confirm a valid refreshed login"
|
||||
# A saved profile must work through the normal command runner,
|
||||
# not only through exchange's in-memory token override.
|
||||
for profile_args in ([], ["--profile", login["data"]["dwsProfile"]]):
|
||||
current = run(["contact", "user", "get-self"] + profile_args, env)
|
||||
identities = current.get("result", [])
|
||||
assert current.get("success") and len(identities) == 1
|
||||
identity = identities[0].get("orgEmployeeModel", {})
|
||||
assert identity.get("corpId") == "fixture-corp" and identity.get("userId") == "fixture-user", \
|
||||
"saved profile could not execute a real DWS command as the employee"
|
||||
segment = calls[before:]
|
||||
assert segment.count("exchange") == 1 and segment.count("identity") == 3
|
||||
assert segment.count("refresh") == 1
|
||||
assert segment.count("discover") == (0 if name == "custom" else 1)
|
||||
checks.append(name + ": exchange -> online identity -> save -> auth status -> refresh -> get-self (default and exact profile)")
|
||||
|
||||
env, directory = environment("validation")
|
||||
before = len(calls)
|
||||
run(["auth", "exchange", "--code-stdin", "--client-id", "default", "--dry-run"], env)
|
||||
run(["auth", "exchange", "--code-stdin", "--mcp"], env, success=False)
|
||||
run(["auth", "exchange", "--code-stdin", "--code", "fixture-code"], env, success=False)
|
||||
assert len(calls) == before and not (directory / "profiles.json").exists()
|
||||
checks.append("dry-run and invalid arguments: no network or profile writes")
|
||||
|
||||
for outcome in ("success", "failure", "dry-run"):
|
||||
for input_mode in ("separate", "equals", "stdin"):
|
||||
env, directory = environment("redaction-" + outcome + "-" + input_mode)
|
||||
state.update(app="official-app", bad_identity=(outcome == "failure"))
|
||||
before = len(calls)
|
||||
code_args = {"separate": ["--code", "fixture-code"],
|
||||
"equals": ["--code=fixture-code"], "stdin": ["--code-stdin"]}[input_mode]
|
||||
run(["auth", "exchange", "--client-id", "default"] + code_args +
|
||||
(["--dry-run"] if outcome == "dry-run" else []), env,
|
||||
"fixture-code\n" if input_mode == "stdin" else "", success=outcome != "failure")
|
||||
if outcome == "dry-run":
|
||||
assert len(calls) == before
|
||||
if outcome != "success":
|
||||
assert not (directory / "profiles.json").exists()
|
||||
checks.append("9 binary redaction cases: success/failure/dry-run x --code VALUE/--code=VALUE/stdin; stdout, stderr, perf report")
|
||||
|
||||
env, directory = environment("missing-identity")
|
||||
state.update(app="custom-app", bad_identity=True)
|
||||
run(["auth", "exchange", "--code-stdin", "--client-id", "custom-app"], env,
|
||||
"fixture-code\n", success=False)
|
||||
assert not (directory / "profiles.json").exists()
|
||||
checks.append("missing online userId: rejected without saving profile")
|
||||
assert not errors, errors
|
||||
print(json.dumps({"success": True, "environment": "localhost fixtures only",
|
||||
"checks": checks}, ensure_ascii=False, indent=2))
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user