@clawos-dev/clawd 0.2.546 → 0.2.548
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/app-builder-plugin/mcp-server.cjs +35 -35
- package/dist/cli.cjs +518 -663
- package/dist/persona-defaults/persona-app-builder/.mcp.json +17 -2
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +8 -9
- package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +6 -7
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +3 -2
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
- package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/publish.sh +3 -3
- package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/remove-extension.sh +2 -2
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +25397 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +14 -1
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +1 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +6 -7
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +1 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +3 -2
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/contract/bootstrap +22 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +65 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +71 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +205 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +89 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +49 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +25397 -0
- package/package.json +1 -1
- package/dist/deploy-kit/.secrets/aliyun.env.example +0 -7
- package/dist/deploy-kit/.secrets/aliyun.env.local.example +0 -12
- package/dist/deploy-kit/scripts/ensure-credentials.sh +0 -111
- package/dist/deploy-kit/scripts/ensure-credentials.spec.ts +0 -438
- package/dist/deploy-kit/scripts/publish.spec.ts +0 -278
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/contract/bootstrap +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/ensure-toolchain.sh +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/new-extension.sh +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/verify.sh +0 -0
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# 阿里云 RAM AccessKey(publish.sh / remove-extension.sh 经 ensure-credentials.sh source)。
|
|
2
|
-
# clawd 不随包 ship 任何一把 AK:把自己的 AK/SK 写进同目录的 aliyun.env.local(推荐,永不被 OTA 覆盖)
|
|
3
|
-
# 或 aliyun.env;两个都不进 git(clawd/.gitignore 只放行 *.example)。云上不用文件,gateway 经 env 注入。
|
|
4
|
-
# 需要权限(按 90 天 ActionTrail 反推):FC(cn-hangzhou)+ SLS 的 serverless-cn-hangzhou-* project + 证书只读;
|
|
5
|
-
# 没见到 OSS 调用(大代码包是否走 OSS 中转未验证,缺了 s deploy 会在 ActionTrail 里报 NoPermission)。
|
|
6
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID=replace-me
|
|
7
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET=replace-me
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# 阿里云 RAM AccessKey(本机专用)
|
|
2
|
-
#
|
|
3
|
-
# 复制这份为同目录下 aliyun.env.local,填自己的 AK/SK。publish.sh / remove-extension.sh 会依次
|
|
4
|
-
# source aliyun.env(如果有)→ aliyun.env.local,后者的值覆盖前者。
|
|
5
|
-
#
|
|
6
|
-
# .local 后缀由 daemon refreshDeployKit 特判:**永远不会被 OTA 覆盖**。
|
|
7
|
-
#
|
|
8
|
-
# 2026-09-18 起 clawd 不再随包 ship 共享 demo AK(上一把随 npm 包发出去 69 秒就被 TruffleHog 收割,
|
|
9
|
-
# 主账号被阿里云风控锁;见 specs/2026-09-18-aliyun-ak-leak-rotate-design.md)——本机发布必须配这份。
|
|
10
|
-
# 老机器上 OTA 留下的 aliyun.env 里那把 AK 已泄露、不可再用,不用删,配了 .local 就会盖过它。
|
|
11
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID=replace-me
|
|
12
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET=replace-me
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ============================================================
|
|
3
|
-
# 发布/清理动手前把阿里云凭证准备好(publish.sh / remove-extension.sh 共用)。
|
|
4
|
-
# 被 `source` 进来,再调 ensure_credentials。跟 ensure-toolchain.sh 是一对:
|
|
5
|
-
# 那个管「命令在不在」,这个管「命令拿什么身份跑」。
|
|
6
|
-
#
|
|
7
|
-
# 硬约束:**全程不碰用户全局 ~/.s**。clawd 发布用的是 deploy-kit 自己那份 AK(本机 .secrets/
|
|
8
|
-
# 下使用者写的文件,云上是 gateway 注入的 env),而 ~/.s/access.yaml 里的 default 是用户自己账号的
|
|
9
|
-
# Serverless Devs 凭证——它是用户的东西,不是我们的。做法对齐本 kit 处理 aliyun CLI 的方式(凭证只进本次进程的 env,
|
|
10
|
-
# 全局 ~/.aliyun/ 一个字节不碰):把 Serverless Devs 的配置家目录整个指到 deploy-kit
|
|
11
|
-
# 名下,access.yaml 写在那里。
|
|
12
|
-
# ============================================================
|
|
13
|
-
|
|
14
|
-
# s 的 profile 名。刻意不叫 default:万一哪天某个版本的 s 不认 serverless_devs_config_home,
|
|
15
|
-
# 隔离失效时它会退回去用户的 ~/.s/access.yaml 里找 clawd-fc —— 找不到就
|
|
16
|
-
# `Not found access: clawd-fc` 硬报错,而不是默默拿用户的 default 凭证把 extension
|
|
17
|
-
# 部署进别人的账号。调 s 时都显式带 `--access "$S_ACCESS_ALIAS"`:命令行参数覆盖 s.yaml
|
|
18
|
-
# 里的 access 字段,本改动前 scaffold 的老项目(s.yaml 写着 default)照样走这条。
|
|
19
|
-
S_ACCESS_ALIAS=clawd-fc
|
|
20
|
-
|
|
21
|
-
# 分层加载阿里云 AK 进 env,三层平铺、后读的覆盖先读的:
|
|
22
|
-
# 1. `$KIT_DIR/.secrets/aliyun.env` —— 使用者自己写的(或 2026-09-18 之前 OTA 留下的旧文件);
|
|
23
|
-
# 2. `$KIT_DIR/.secrets/aliyun.env.local` —— 使用者自己写的,refreshDeployKit 永不覆盖;
|
|
24
|
-
# 3. 进程 env 里已有的 `ALIBABA_CLOUD_ACCESS_KEY_ID/_SECRET` —— 云上容器那条路:Pod 里没有
|
|
25
|
-
# `.secrets/`(`cloud-host/provision/build-image.mjs` 刻意把它剔出镜像层——镜像要分发,凭据
|
|
26
|
-
# 进层就洗不掉了),AK 由 gateway 经 **MCP server 的 per-server env** 交到调用本脚本的那个
|
|
27
|
-
# 进程里(M1b Step 9)。不落盘是有意的:写成文件的话 agent 一个 `cat` 就看见了。
|
|
28
|
-
#
|
|
29
|
-
# clawd **不再随包 ship 任何一把 AK**(2026-09-18 起,见 specs/2026-09-18-aliyun-ak-leak-rotate-design.md:
|
|
30
|
-
# 上一把随 npm 包发出去 69 秒就被 TruffleHog 收割,主账号被风控锁)。三层都空 = 使用者还没配,fail-loud。
|
|
31
|
-
ensure_aliyun_env() {
|
|
32
|
-
local kit="${KIT_DIR:?ensure_aliyun_env 需要调用方先设 KIT_DIR}"
|
|
33
|
-
local f
|
|
34
|
-
# 写成 if 而不是 `[ -f ] && { }`:后者在函数末尾会把「文件不存在」变成函数返回 1,调用方 set -e 直接中止。
|
|
35
|
-
for f in "$kit/.secrets/aliyun.env" "$kit/.secrets/aliyun.env.local"; do
|
|
36
|
-
if [ -f "$f" ]; then
|
|
37
|
-
set -a; . "$f"; set +a
|
|
38
|
-
fi
|
|
39
|
-
done
|
|
40
|
-
if [ -z "${ALIBABA_CLOUD_ACCESS_KEY_ID:-}" ] || [ -z "${ALIBABA_CLOUD_ACCESS_KEY_SECRET:-}" ]; then
|
|
41
|
-
echo "❌ 没有阿里云 AK:把自己的 AK/SK 写进 $kit/.secrets/aliyun.env.local(参考同目录 aliyun.env.local.example;这个文件不会被 OTA 覆盖),云上则由 gateway 经 env 注入。" >&2
|
|
42
|
-
return 1
|
|
43
|
-
fi
|
|
44
|
-
export ALIBABA_CLOUD_REGION_ID="${REGION:?config.env 缺 REGION}"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# 调 aliyun CLI 的**唯一入口**:把凭证和 region 作为显式参数交出去,不依赖环境变量的隐式优先级。
|
|
48
|
-
#
|
|
49
|
-
# 为什么非这样不可(实测 aliyun 3.3.17,`--mode AK`):`ALIBABA_CLOUD_ACCESS_KEY_ID` /
|
|
50
|
-
# `_SECRET` / `_REGION_ID` 这三个环境变量在这个模式下**根本不被消费**,于是
|
|
51
|
-
# - 机器上没有 `~/.aliyun/config.json`(全新用户的常态,ensure_toolchain 只装 CLI、
|
|
52
|
-
# 从不跑 `aliyun configure`)→ `ERROR: region can't be empty`,第一次发布就跑不起来;
|
|
53
|
-
# - 机器上有 profile(比如用户自己配过阿里云)→ **静默走用户自己的 profile**,等于拿
|
|
54
|
-
# 用户的阿里云身份去部署 extension,跟 access.yaml 那个坑是同一类。
|
|
55
|
-
# 补 `--region` 一个 flag 不够,AK 也必须是 flag——两者都试过。
|
|
56
|
-
#
|
|
57
|
-
# 用法跟 aliyun 一样,省掉 `--mode AK` 和三个凭证参数:aliyun_ak sts GetCallerIdentity
|
|
58
|
-
aliyun_ak() {
|
|
59
|
-
aliyun --mode AK \
|
|
60
|
-
--access-key-id "${ALIBABA_CLOUD_ACCESS_KEY_ID:?aliyun_ak 需要先 ensure_aliyun_env}" \
|
|
61
|
-
--access-key-secret "${ALIBABA_CLOUD_ACCESS_KEY_SECRET:?aliyun_ak 需要先 ensure_aliyun_env}" \
|
|
62
|
-
--region "${ALIBABA_CLOUD_REGION_ID:?aliyun_ak 需要先 ensure_aliyun_env}" \
|
|
63
|
-
"$@"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# 把 Serverless Devs 的配置家目录(access.yaml / 组件缓存 / 日志)指到 deploy-kit 名下,
|
|
67
|
-
# 再把当前 env 里的凭证写成 $S_ACCESS_ALIAS profile。
|
|
68
|
-
# s v3 的 rootHome = `$serverless_devs_config_home/.s`(默认 `$HOME/.s`),`s -v` 打印的 s-home 就是它——
|
|
69
|
-
# 注意差一层:env 指到 X,文件落 X/.s/,所以下面是 "$s_home/.s/access.yaml" 而不是 "$s_home/access.yaml"。
|
|
70
|
-
# 前置:ensure_toolchain(aliyun 可执行)+ ensure_aliyun_env(AK 进 env)。
|
|
71
|
-
# 放 $KIT_DIR 下而不是另起路径:跟它写的那份凭证同源,且 refreshDeployKit 只覆盖
|
|
72
|
-
# scripts/ contract/ .secrets/,OTA 不会动这里的组件缓存。
|
|
73
|
-
ensure_s_access() {
|
|
74
|
-
local kit="${KIT_DIR:?ensure_s_access 需要调用方先设 KIT_DIR}"
|
|
75
|
-
local s_home="$kit/.s-home"
|
|
76
|
-
local ident acc
|
|
77
|
-
|
|
78
|
-
ident="$(aliyun_ak sts GetCallerIdentity 2>&1)" || {
|
|
79
|
-
# 别只说「检查凭证」——凭证不对只是可能之一,region 缺失 / CLI 装坏 / 网络不通报的是别的话。
|
|
80
|
-
# 把 CLI 原文摆出来,让读的人按原文判断。
|
|
81
|
-
echo "❌ aliyun sts GetCallerIdentity 失败,拿不到 AccountId。aliyun 原文:" >&2
|
|
82
|
-
echo "$ident" >&2
|
|
83
|
-
echo " 排查顺序:① 上面这行 CLI 原文说的是什么 ② $kit/.secrets/aliyun.env.local(或 aliyun.env)里的 AK 是否有效——2026-09-18 之前 OTA 发的那把已泄露、不可再用,得换自己的 ③ config.env 的 REGION" >&2
|
|
84
|
-
return 1
|
|
85
|
-
}
|
|
86
|
-
# 用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有 python
|
|
87
|
-
# —— 清理路径尤其不该多一个装不上就跑不了的依赖。
|
|
88
|
-
acc="$(printf '%s' "$ident" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).AccountId)')" || {
|
|
89
|
-
echo "❌ 解析 AccountId 失败,aliyun 输出非 JSON:" >&2
|
|
90
|
-
echo "$ident" >&2
|
|
91
|
-
return 1
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export serverless_devs_config_home="$s_home"
|
|
95
|
-
mkdir -p "$s_home/.s"
|
|
96
|
-
# umask 管「新建」:SK 从落盘第一个字节起就是 0600,不留「先 644 写完再 chmod」的窗口。
|
|
97
|
-
# 后面的 chmod 管「文件已存在」:umask 不改已有文件的 mode,重复发布时靠它兜底。
|
|
98
|
-
( umask 077; cat > "$s_home/.s/access.yaml" <<EOF
|
|
99
|
-
$S_ACCESS_ALIAS:
|
|
100
|
-
AccountID: '$acc'
|
|
101
|
-
AccessKeyID: $ALIBABA_CLOUD_ACCESS_KEY_ID
|
|
102
|
-
AccessKeySecret: $ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
|
103
|
-
EOF
|
|
104
|
-
)
|
|
105
|
-
chmod 600 "$s_home/.s/access.yaml"
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
ensure_credentials() {
|
|
109
|
-
ensure_aliyun_env
|
|
110
|
-
ensure_s_access
|
|
111
|
-
}
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { execFileSync } from 'node:child_process'
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync, cpSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
|
|
7
|
-
// ensure-credentials.sh 的契约测试。核心不变量:**发布/清理全程不碰用户全局 ~/.s**。
|
|
8
|
-
// 老实现 `cat > ~/.s/access.yaml` 会把用户自己的 Serverless Devs default 凭证顶掉且不可恢复
|
|
9
|
-
// (所有装了 clawd 的用户点一次「发布上线」就中招),这些用例就是钉死那个回归。
|
|
10
|
-
//
|
|
11
|
-
// 真脚本 + 真 bash 跑,只把两个外部 CLI(aliyun / s)换成记录 argv 的 shim:
|
|
12
|
-
// 断言的是脚本自己的行为(写哪、写什么、传什么 flag),没有 mock 掉被测逻辑。
|
|
13
|
-
|
|
14
|
-
const SCRIPTS_DIR = __dirname
|
|
15
|
-
|
|
16
|
-
interface Sandbox {
|
|
17
|
-
root: string
|
|
18
|
-
kit: string
|
|
19
|
-
home: string
|
|
20
|
-
bin: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let box: Sandbox
|
|
24
|
-
|
|
25
|
-
/** 用户自己的 ~/.s/access.yaml —— 全程必须一个字节都不变。 */
|
|
26
|
-
const USER_ACCESS_YAML = `default:
|
|
27
|
-
AccountID: '1999888877776666'
|
|
28
|
-
AccessKeyID: LTAI-USER-OWN-KEY
|
|
29
|
-
AccessKeySecret: user-own-secret
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* aliyun CLI shim —— **照真 CLI 的凭证优先级建模**(实测 aliyun 3.3.17,`--mode AK`):
|
|
34
|
-
*
|
|
35
|
-
* - `--access-key-id` / `--access-key-secret` / `--region` 三个 **flag 缺一不可**;
|
|
36
|
-
* - `ALIBABA_CLOUD_*` 环境变量在这个模式下**根本不被消费**——没有 `~/.aliyun/config.json`
|
|
37
|
-
* 的干净机器上只会得到 `region can't be empty`(rc=3),有 profile 的机器上则**静默走用户
|
|
38
|
-
* 自己的 profile**(等于拿用户身份部署)。
|
|
39
|
-
*
|
|
40
|
-
* 所以 shim 只认 flag、不认 env:脚本要是退回去依赖 env 优先级,这里当场红。
|
|
41
|
-
*/
|
|
42
|
-
function writeAliyunShim(bin: string, accountId = '5566778899001122', argvLog?: string): void {
|
|
43
|
-
writeFileSync(
|
|
44
|
-
join(bin, 'aliyun'),
|
|
45
|
-
`#!/usr/bin/env bash
|
|
46
|
-
${argvLog ? `printf '%s\\n' "$*" >> "${argvLog}"` : ''}
|
|
47
|
-
argv=" $* "
|
|
48
|
-
for f in --access-key-id --access-key-secret --region; do
|
|
49
|
-
case "$argv" in
|
|
50
|
-
*" $f "*) ;;
|
|
51
|
-
*) echo "ERROR: region can't be empty" >&2; exit 3 ;;
|
|
52
|
-
esac
|
|
53
|
-
done
|
|
54
|
-
case "$argv" in
|
|
55
|
-
*" sts "*) printf '{"AccountId":"${accountId}","Arn":"acs:ram::${accountId}:user/clawd-fc-developer"}\\n' ;;
|
|
56
|
-
*) printf '{"customDomains":[]}\\n' ;;
|
|
57
|
-
esac
|
|
58
|
-
`,
|
|
59
|
-
{ mode: 0o755 },
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** s shim:把每次调用的 argv 追加到 $S_ARGV_LOG,供断言 --access 传对了没。 */
|
|
64
|
-
function writeSShim(bin: string, log: string): void {
|
|
65
|
-
writeFileSync(
|
|
66
|
-
join(bin, 's'),
|
|
67
|
-
`#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> "${log}"\n`,
|
|
68
|
-
{ mode: 0o755 },
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
const root = mkdtempSync(join(tmpdir(), 'ensure-creds-'))
|
|
74
|
-
const kit = join(root, 'deploy-kit')
|
|
75
|
-
const home = join(root, 'home')
|
|
76
|
-
const bin = join(root, 'bin')
|
|
77
|
-
mkdirSync(join(kit, '.secrets'), { recursive: true })
|
|
78
|
-
mkdirSync(join(home, '.s'), { recursive: true })
|
|
79
|
-
mkdirSync(bin, { recursive: true })
|
|
80
|
-
|
|
81
|
-
// 真脚本进沙箱 kit。.secrets 按生产形状:bundle 只带 *.example(2026-09-18 起 AK 不再随包
|
|
82
|
-
// 分发,见 specs/2026-09-18-aliyun-ak-leak-rotate-design.md),本机凭证是使用者自己写的 .local。
|
|
83
|
-
cpSync(SCRIPTS_DIR, join(kit, 'scripts'), { recursive: true })
|
|
84
|
-
writeFileSync(
|
|
85
|
-
join(kit, '.secrets', 'aliyun.env.local'),
|
|
86
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-MY-OWN\nALIBABA_CLOUD_ACCESS_KEY_SECRET=my-own-secret\n',
|
|
87
|
-
)
|
|
88
|
-
writeFileSync(join(home, '.s', 'access.yaml'), USER_ACCESS_YAML)
|
|
89
|
-
writeAliyunShim(bin)
|
|
90
|
-
|
|
91
|
-
box = { root, kit, home, bin }
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
rmSync(box.root, { recursive: true, force: true })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
/** 在沙箱里 source ensure-credentials.sh 并跑一段 bash,返回 stdout。 */
|
|
99
|
-
function runInKit(body: string): string {
|
|
100
|
-
const script = `
|
|
101
|
-
set -euo pipefail
|
|
102
|
-
KIT_DIR="${box.kit}"
|
|
103
|
-
REGION="\${REGION:-cn-hangzhou}"
|
|
104
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
105
|
-
${body}
|
|
106
|
-
`
|
|
107
|
-
return execFileSync('bash', ['-c', script], {
|
|
108
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
109
|
-
encoding: 'utf8',
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function userAccessYaml(): string {
|
|
114
|
-
return readFileSync(join(box.home, '.s', 'access.yaml'), 'utf8')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function kitAccessYaml(): string {
|
|
118
|
-
return readFileSync(join(box.kit, '.s-home', '.s', 'access.yaml'), 'utf8')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
describe('ensure_credentials', () => {
|
|
122
|
-
it('不碰用户全局 ~/.s/access.yaml', () => {
|
|
123
|
-
runInKit('ensure_credentials')
|
|
124
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('凭证写进 kit 私有 s-home', () => {
|
|
128
|
-
runInKit('ensure_credentials')
|
|
129
|
-
const yaml = kitAccessYaml()
|
|
130
|
-
expect(yaml).toContain("AccountID: '5566778899001122'")
|
|
131
|
-
expect(yaml).toContain('AccessKeyID: LTAI-MY-OWN')
|
|
132
|
-
expect(yaml).toContain('AccessKeySecret: my-own-secret')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('profile 名是 clawd-fc 而不是 default', () => {
|
|
136
|
-
// 隔离万一失效(某版本 s 不认 serverless_devs_config_home)时,s 会去用户
|
|
137
|
-
// ~/.s/access.yaml 找 clawd-fc —— 找不到就硬报错,而不是默默拿用户的 default
|
|
138
|
-
// 凭证把 extension 部署进别人的账号。
|
|
139
|
-
runInKit('ensure_credentials')
|
|
140
|
-
expect(kitAccessYaml()).toMatch(/^clawd-fc:$/m)
|
|
141
|
-
expect(kitAccessYaml()).not.toMatch(/^default:$/m)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('把 Serverless Devs 的家目录指向 kit 私有目录', () => {
|
|
145
|
-
const out = runInKit('ensure_credentials; echo "S_HOME=$serverless_devs_config_home"')
|
|
146
|
-
expect(out).toContain(`S_HOME=${box.kit}/.s-home`)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('access.yaml 权限 600(含 SK,不给同机其它用户读)', () => {
|
|
150
|
-
runInKit('ensure_credentials')
|
|
151
|
-
const mode = statSync(join(box.kit, '.s-home', '.s', 'access.yaml')).mode & 0o777
|
|
152
|
-
expect(mode.toString(8)).toBe('600')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// 存量机器:OTA 停发 aliyun.env 之后 refreshDeployKit 不再覆盖也不删它,旧文件会留在盘上。
|
|
156
|
-
// 有 .local 时 .local 必须盖过它(否则老用户配了自己的 AK 还在用那把已停用的)。
|
|
157
|
-
it('legacy aliyun.env 仍被读,但 .local 覆盖它', () => {
|
|
158
|
-
writeFileSync(
|
|
159
|
-
join(box.kit, '.secrets', 'aliyun.env'),
|
|
160
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-STALE-OTA\nALIBABA_CLOUD_ACCESS_KEY_SECRET=stale-ota-secret\n',
|
|
161
|
-
)
|
|
162
|
-
runInKit('ensure_credentials')
|
|
163
|
-
expect(kitAccessYaml()).toContain('AccessKeyID: LTAI-MY-OWN')
|
|
164
|
-
expect(kitAccessYaml()).not.toContain('LTAI-STALE-OTA')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('只有 aliyun.env 没有 .local 也能用(使用者直接写 aliyun.env 的老习惯)', () => {
|
|
168
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
169
|
-
writeFileSync(
|
|
170
|
-
join(box.kit, '.secrets', 'aliyun.env'),
|
|
171
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-PLAIN-FILE\nALIBABA_CLOUD_ACCESS_KEY_SECRET=plain-file-secret\n',
|
|
172
|
-
)
|
|
173
|
-
runInKit('ensure_credentials')
|
|
174
|
-
expect(kitAccessYaml()).toContain('AccessKeyID: LTAI-PLAIN-FILE')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('调 aliyun 时显式传 AK 和 region,不靠环境变量的隐式优先级', () => {
|
|
178
|
-
// 真 CLI 在 `--mode AK` 下不消费 ALIBABA_CLOUD_*:干净机器上报 `region can't be empty`
|
|
179
|
-
// 直接跑不起来;有 ~/.aliyun/config.json 的机器上更坏——静默走用户自己的 profile,
|
|
180
|
-
// 等于拿用户的阿里云身份去部署。两种都必须由显式 flag 堵死。
|
|
181
|
-
const log = join(box.root, 'aliyun-argv.log')
|
|
182
|
-
writeAliyunShim(box.bin, '5566778899001122', log)
|
|
183
|
-
runInKit('ensure_credentials')
|
|
184
|
-
const argv = readFileSync(log, 'utf8')
|
|
185
|
-
expect(argv).toContain('--access-key-id LTAI-MY-OWN')
|
|
186
|
-
expect(argv).toContain('--access-key-secret my-own-secret')
|
|
187
|
-
expect(argv).toContain('--region cn-hangzhou')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('机器上没有 aliyun profile 也能拿到 AccountId', () => {
|
|
191
|
-
// 全新用户机器:ensure_toolchain 只装 CLI、从不跑 `aliyun configure`,所以 HOME 下
|
|
192
|
-
// 没有 .aliyun/config.json。严格 shim 复现真 CLI 的这条路径。
|
|
193
|
-
rmSync(join(box.home, '.aliyun'), { recursive: true, force: true })
|
|
194
|
-
runInKit('ensure_credentials')
|
|
195
|
-
expect(kitAccessYaml()).toContain("AccountID: '5566778899001122'")
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('aliyun.env / .local 都没有、env 里也没有 AK 时 fail-loud,文案指到 .local', () => {
|
|
199
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
200
|
-
expect(() => runInKit('ensure_credentials')).toThrow(/aliyun\.env\.local/)
|
|
201
|
-
expect(existsSync(join(box.kit, '.s-home', '.s', 'access.yaml'))).toBe(false)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// 云上分支(persona 上云 M1b Step 9):容器里没有 `.secrets/`——`build-image.mjs` 刻意把它
|
|
205
|
-
// 剔出镜像层(镜像要分发,凭据进层洗不掉)。AK 由 gateway 经 **MCP server 的 per-server env**
|
|
206
|
-
// 交到调用本脚本的那个子进程里,不落文件:写成文件的话 agent 一个 `cat` 就看见了。
|
|
207
|
-
it('没有 .secrets/ 凭证文件但 env 里有 AK 时用 env(云上容器那条路)', () => {
|
|
208
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
209
|
-
const out = execFileSync(
|
|
210
|
-
'bash',
|
|
211
|
-
[
|
|
212
|
-
'-c',
|
|
213
|
-
`set -euo pipefail
|
|
214
|
-
KIT_DIR="${box.kit}"
|
|
215
|
-
REGION=cn-hangzhou
|
|
216
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
217
|
-
ensure_credentials
|
|
218
|
-
printf 'ok %s\\n' "$ALIBABA_CLOUD_ACCESS_KEY_ID"`,
|
|
219
|
-
],
|
|
220
|
-
{
|
|
221
|
-
env: {
|
|
222
|
-
...process.env,
|
|
223
|
-
HOME: box.home,
|
|
224
|
-
PATH: `${box.bin}:${process.env.PATH}`,
|
|
225
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID: 'LTAI-FROM-ENV',
|
|
226
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'secret-from-env',
|
|
227
|
-
},
|
|
228
|
-
encoding: 'utf8',
|
|
229
|
-
},
|
|
230
|
-
)
|
|
231
|
-
expect(out).toContain('ok LTAI-FROM-ENV')
|
|
232
|
-
// 走 env 分支照样把凭证落进 kit 私有 s-home(publish 用的是那份 profile),仍不碰用户全局
|
|
233
|
-
expect(kitAccessYaml()).toContain('LTAI-FROM-ENV')
|
|
234
|
-
expect(userAccessYaml()).toContain('LTAI-USER-OWN-KEY')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('文件在就优先用文件(云上那条只是兜底,不能反过来盖掉本机 .local 凭证)', () => {
|
|
238
|
-
const out = execFileSync(
|
|
239
|
-
'bash',
|
|
240
|
-
[
|
|
241
|
-
'-c',
|
|
242
|
-
`set -euo pipefail
|
|
243
|
-
KIT_DIR="${box.kit}"
|
|
244
|
-
REGION=cn-hangzhou
|
|
245
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
246
|
-
ensure_aliyun_env
|
|
247
|
-
printf 'id=%s\\n' "$ALIBABA_CLOUD_ACCESS_KEY_ID"`,
|
|
248
|
-
],
|
|
249
|
-
{
|
|
250
|
-
env: {
|
|
251
|
-
...process.env,
|
|
252
|
-
HOME: box.home,
|
|
253
|
-
PATH: `${box.bin}:${process.env.PATH}`,
|
|
254
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID: 'LTAI-FROM-ENV',
|
|
255
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'secret-from-env',
|
|
256
|
-
},
|
|
257
|
-
encoding: 'utf8',
|
|
258
|
-
},
|
|
259
|
-
)
|
|
260
|
-
expect(out).toContain('id=LTAI-MY-OWN')
|
|
261
|
-
expect(out).not.toContain('LTAI-FROM-ENV')
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
describe('publish.sh', () => {
|
|
266
|
-
/** 最小 persona kit + 项目目录;模板只留跟本文件断言相关的占位符。 */
|
|
267
|
-
function publishFixture(): { ext: string; persona: string } {
|
|
268
|
-
// 严格 shim 的自定义域名查询回空 → publish.sh 在 verify 段 exit 1。deploy 之后的事,
|
|
269
|
-
// 不影响本用例要断言的凭证 / 渲染行为,省掉 curl shim。
|
|
270
|
-
writeAliyunShim(box.bin, '5566778899001122', join(box.root, 'aliyun-argv.log'))
|
|
271
|
-
const ext = join(box.root, 'proj')
|
|
272
|
-
const persona = join(box.root, 'persona')
|
|
273
|
-
mkdirSync(join(ext, 'server'), { recursive: true })
|
|
274
|
-
mkdirSync(join(persona, 'extension-kit', 'contract'), { recursive: true })
|
|
275
|
-
mkdirSync(join(box.kit, 'contract'), { recursive: true })
|
|
276
|
-
writeFileSync(join(box.kit, 'contract', 'bootstrap'), '#!/usr/bin/env bash\n')
|
|
277
|
-
writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\nCODE_DIR=./server\n')
|
|
278
|
-
writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
|
|
279
|
-
writeFileSync(
|
|
280
|
-
join(persona, 'extension-kit', 'contract', 's.yaml.tmpl'),
|
|
281
|
-
'name: __DEPLOY_NAME__-app\naccess: clawd-fc\nenv: __APP_ENV__\nprefix: __TABLE_PREFIX__\n',
|
|
282
|
-
)
|
|
283
|
-
return { ext, persona }
|
|
284
|
-
}
|
|
285
|
-
function runPublish(ext: string, persona: string, env: string): void {
|
|
286
|
-
expect(() =>
|
|
287
|
-
execFileSync('bash', [join(box.kit, 'scripts', 'publish.sh'), ext, persona, env], {
|
|
288
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
289
|
-
encoding: 'utf8',
|
|
290
|
-
stdio: 'pipe',
|
|
291
|
-
}),
|
|
292
|
-
).toThrow() // verify 段无域名 → exit 1,deploy 已经跑过了
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
it('用 clawd-fc profile 调 s deploy,不碰用户全局 ~/.s', () => {
|
|
296
|
-
const log = join(box.root, 's-argv.log')
|
|
297
|
-
writeSShim(box.bin, log)
|
|
298
|
-
const { ext, persona } = publishFixture()
|
|
299
|
-
runPublish(ext, persona, 'integration')
|
|
300
|
-
expect(readFileSync(log, 'utf8')).toContain('deploy -y -t s.int.yaml --access clawd-fc')
|
|
301
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// 双环境(spec 2026-09-15-app-builder-integration-env-design §3.2):同一个项目两个 env 各渲各的
|
|
305
|
-
// s 文件、各自的 DEPLOY_NAME / TABLE_PREFIX,prod 那份跟双环境之前的值一字不差(老地址不动)。
|
|
306
|
-
it('integration → s.int.yaml(-int / _int_),prod → s.prod.yaml(老值),两份并存互不覆盖', () => {
|
|
307
|
-
const log = join(box.root, 's-argv.log')
|
|
308
|
-
writeSShim(box.bin, log)
|
|
309
|
-
const { ext, persona } = publishFixture()
|
|
310
|
-
runPublish(ext, persona, 'integration')
|
|
311
|
-
runPublish(ext, persona, 'prod')
|
|
312
|
-
expect(readFileSync(join(ext, 's.int.yaml'), 'utf8')).toBe('name: demo-ab12-int-app\naccess: clawd-fc\nenv: integration\nprefix: demo_ab12_int_\n')
|
|
313
|
-
expect(readFileSync(join(ext, 's.prod.yaml'), 'utf8')).toBe('name: demo-ab12-app\naccess: clawd-fc\nenv: prod\nprefix: demo_ab12_\n')
|
|
314
|
-
expect(existsSync(join(ext, 's.yaml'))).toBe(false)
|
|
315
|
-
const argv = readFileSync(log, 'utf8')
|
|
316
|
-
expect(argv).toContain('deploy -y -t s.int.yaml --access clawd-fc')
|
|
317
|
-
expect(argv).toContain('deploy -y -t s.prod.yaml --access clawd-fc')
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
// 存量项目的 .gitignore 是 scaffold 时拷的一次性文件,只有 `s.yaml`:s.int.yaml 渲下去会被当成源码推上 GitHub
|
|
321
|
-
//(里面有 supabase key)。publish.sh 像 bake SLUG 那样幂等地补一行,模板项目已有的不重复。
|
|
322
|
-
it('项目 .gitignore 没有 s.*.yaml 就补上(幂等);没有 .gitignore 也建', () => {
|
|
323
|
-
writeSShim(box.bin, join(box.root, 's-argv.log'))
|
|
324
|
-
const { ext, persona } = publishFixture()
|
|
325
|
-
writeFileSync(join(ext, '.gitignore'), 'node_modules/\ns.yaml\n')
|
|
326
|
-
runPublish(ext, persona, 'integration')
|
|
327
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('node_modules/\ns.yaml\ns.*.yaml\n')
|
|
328
|
-
runPublish(ext, persona, 'prod')
|
|
329
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('node_modules/\ns.yaml\ns.*.yaml\n')
|
|
330
|
-
rmSync(join(ext, '.gitignore'))
|
|
331
|
-
runPublish(ext, persona, 'integration')
|
|
332
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('s.*.yaml\n')
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it('缺 env / 非法 env:渲染和 deploy 都不跑', () => {
|
|
336
|
-
const log = join(box.root, 's-argv.log')
|
|
337
|
-
writeSShim(box.bin, log)
|
|
338
|
-
const { ext, persona } = publishFixture()
|
|
339
|
-
for (const args of [[ext, persona], [ext, persona, 'staging']]) {
|
|
340
|
-
expect(() =>
|
|
341
|
-
execFileSync('bash', [join(box.kit, 'scripts', 'publish.sh'), ...args], {
|
|
342
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
343
|
-
encoding: 'utf8',
|
|
344
|
-
stdio: 'pipe',
|
|
345
|
-
}),
|
|
346
|
-
).toThrow(/integration\|prod/)
|
|
347
|
-
}
|
|
348
|
-
expect(existsSync(log)).toBe(false)
|
|
349
|
-
expect(existsSync(join(ext, 's.int.yaml'))).toBe(false)
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
describe('remove-extension.sh', () => {
|
|
354
|
-
function removeFixture(opts: { sFiles: string[]; tables?: string; names?: Record<string, string> }): { ext: string; persona: string } {
|
|
355
|
-
const ext = join(box.root, 'proj')
|
|
356
|
-
const persona = join(box.root, 'persona')
|
|
357
|
-
mkdirSync(ext, { recursive: true })
|
|
358
|
-
mkdirSync(join(persona, 'extension-kit'), { recursive: true })
|
|
359
|
-
writeFileSync(join(ext, 'ext.conf'), `APP_NAME=demo\nSLUG=ab12\n${opts.tables !== undefined ? `SUPABASE_TABLES="${opts.tables}"\n` : ''}`)
|
|
360
|
-
writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
|
|
361
|
-
for (const f of opts.sFiles) writeFileSync(join(ext, f), `name: ${opts.names?.[f] ?? 'x'}\n`)
|
|
362
|
-
return { ext, persona }
|
|
363
|
-
}
|
|
364
|
-
function runRemove(ext: string, persona: string): string {
|
|
365
|
-
return execFileSync('bash', [join(box.kit, 'scripts', 'remove-extension.sh'), ext, persona], {
|
|
366
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
367
|
-
encoding: 'utf8',
|
|
368
|
-
})
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
it('用 clawd-fc profile 调 s remove,不碰用户全局 ~/.s', () => {
|
|
372
|
-
const log = join(box.root, 's-argv.log')
|
|
373
|
-
writeSShim(box.bin, log)
|
|
374
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.int.yaml'] })
|
|
375
|
-
runRemove(ext, persona)
|
|
376
|
-
expect(readFileSync(log, 'utf8')).toContain('remove -y -t s.int.yaml --access clawd-fc')
|
|
377
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
// 双环境:两个 env 各 s remove 一次(哪份 s 文件在删哪份;legacy s.yaml 也认),DROP 打两套前缀
|
|
381
|
-
it('s.int.yaml / s.prod.yaml 各 remove 一次(legacy s.yaml 与 s.prod.yaml 同指 prod 函数,有后者就不再对前者 remove);基础表名展开成 _int_ 与无标签两套 DROP', () => {
|
|
382
|
-
const log = join(box.root, 's-argv.log')
|
|
383
|
-
writeSShim(box.bin, log)
|
|
384
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.int.yaml', 's.prod.yaml', 's.yaml'], tables: 'visits notes' })
|
|
385
|
-
const out = runRemove(ext, persona)
|
|
386
|
-
const argv = readFileSync(log, 'utf8')
|
|
387
|
-
expect(argv).toContain('remove -y -t s.int.yaml --access clawd-fc')
|
|
388
|
-
expect(argv).toContain('remove -y -t s.prod.yaml --access clawd-fc')
|
|
389
|
-
expect(argv).not.toContain('-t s.yaml ')
|
|
390
|
-
for (const t of ['demo_ab12_int_visits', 'demo_ab12_visits', 'demo_ab12_int_notes', 'demo_ab12_notes']) {
|
|
391
|
-
expect(out).toContain(`DROP TABLE IF EXISTS public.${t} CASCADE;`)
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
it('双环境之前的完整表名(带完整前缀 <APP_NAME>_<SLUG>_)只打一条;别的前缀一律按基础名展开,DROP 永远落在自己前缀下', () => {
|
|
396
|
-
const log = join(box.root, 's-argv.log')
|
|
397
|
-
writeSShim(box.bin, log)
|
|
398
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.yaml'], tables: 'demo_ab12_visits' })
|
|
399
|
-
const out = runRemove(ext, persona)
|
|
400
|
-
expect(readFileSync(log, 'utf8')).toContain('remove -y -t s.yaml --access clawd-fc')
|
|
401
|
-
expect(out.match(/DROP TABLE/g)).toHaveLength(1)
|
|
402
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_visits CASCADE;')
|
|
403
|
-
writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\nSUPABASE_TABLES="demo_other_visits"\n')
|
|
404
|
-
const out2 = runRemove(ext, persona)
|
|
405
|
-
expect(out2).not.toContain('public.demo_other_visits ')
|
|
406
|
-
expect(out2.match(/DROP TABLE IF EXISTS public\.demo_ab12_(int_)?demo_other_visits CASCADE;/g)).toHaveLength(2)
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// dataclaw 双环境之前 functionName 不带 slug:老 s.yaml 与 s.prod.yaml 指**不同**函数(name: 不同),两份都要拆
|
|
410
|
-
it('老 s.yaml 的 name 与 s.prod.yaml 不同(指不同函数):两份都 remove', () => {
|
|
411
|
-
const log = join(box.root, 's-argv.log')
|
|
412
|
-
writeSShim(box.bin, log)
|
|
413
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.prod.yaml', 's.yaml'], names: { 's.yaml': 'demo-app', 's.prod.yaml': 'demo-ab12-app' } })
|
|
414
|
-
runRemove(ext, persona)
|
|
415
|
-
const argv = readFileSync(log, 'utf8')
|
|
416
|
-
expect(argv).toContain('remove -y -t s.prod.yaml --access clawd-fc')
|
|
417
|
-
expect(argv).toContain('remove -y -t s.yaml --access clawd-fc')
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// #4:基础名恰好以 <APP_NAME>_ 开头(shop 的 shop_items)不能被当成 legacy 完整名——legacy 判据是完整前缀 <APP_NAME>_<SLUG>_
|
|
421
|
-
it('基础名以 <APP_NAME>_ 开头(shop_items)仍按基础名展开两套', () => {
|
|
422
|
-
writeSShim(box.bin, join(box.root, 's-argv.log'))
|
|
423
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.prod.yaml'], tables: 'demo_items' })
|
|
424
|
-
const out = runRemove(ext, persona)
|
|
425
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_int_demo_items CASCADE;')
|
|
426
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_demo_items CASCADE;')
|
|
427
|
-
expect(out.match(/DROP TABLE/g)).toHaveLength(2)
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('从未发布过(没有任何 s 文件):不调 s remove,退出 0', () => {
|
|
431
|
-
const log = join(box.root, 's-argv.log')
|
|
432
|
-
writeSShim(box.bin, log)
|
|
433
|
-
const { ext, persona } = removeFixture({ sFiles: [] })
|
|
434
|
-
const out = runRemove(ext, persona)
|
|
435
|
-
expect(existsSync(log)).toBe(false)
|
|
436
|
-
expect(out).toContain('从未发布过')
|
|
437
|
-
})
|
|
438
|
-
})
|