Merge authoritative main into the task branch
Why: include the current integration baseline before verification. Evidence: fetched origin/main. The multi-instance account bootstrap conflict keeps main's identity ensure and activation flow. Impact: integrate upstream changes without rewriting task history. Backend gitlink stays 80bb123. Refs: task-sync Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,6 +36,13 @@ Agent-System-Dev 是桌面优先、Web 兼容的 AI 工作台。界面应**冷
|
||||
| `--bg-hover` | `rgba(255,255,255,.05)` | `rgba(13,13,13,.05)` | 通用悬停 |
|
||||
| `--bg-active` | `rgba(255,255,255,.10)` | `rgba(13,13,13,.08)` | 选中态 |
|
||||
|
||||
三栏工作台的顶栏与左栏使用独立导航骨架令牌:`--workbench-chrome`、
|
||||
`--workbench-chrome-text`、`--workbench-chrome-dim`、`--workbench-chrome-faint`、
|
||||
`--workbench-chrome-hover`、`--workbench-chrome-active`。浅色主题为近黑导航骨架,深色主题取面板阶梯,
|
||||
墨绿主题取海军蓝侧栏;反色创建按钮的 hover/分隔线使用
|
||||
`--workbench-chrome-inverse-hover` / `--workbench-chrome-inverse-border`。宿主组件只能消费这些令牌,
|
||||
不得用 `color-mix()`、rgba 或局部变量再次派生导航颜色。
|
||||
|
||||
### 2.2 边框(发丝三档)
|
||||
|
||||
`--border-soft: rgba(255,255,255,.06)` / `--border: rgba(255,255,255,.10)` /
|
||||
|
||||
+1
-1
Submodule modules/frontend updated: 710ef836b5...5ab81432d1
+1
-1
Submodule modules/ui updated: 7c35a78abc...e295caeef3
@@ -45,10 +45,11 @@ const (
|
||||
)
|
||||
|
||||
type instance struct {
|
||||
name string
|
||||
base string
|
||||
cmd *exec.Cmd
|
||||
log *lockedBuffer
|
||||
name string
|
||||
base string
|
||||
dataDir string // serve 的 cwd:本地实例身份(manifest)所在,账户 CLI 须以此身份运行
|
||||
cmd *exec.Cmd
|
||||
log *lockedBuffer
|
||||
}
|
||||
|
||||
type lockedBuffer struct {
|
||||
@@ -311,6 +312,20 @@ func (e *env) startPair() {
|
||||
e.t.Helper()
|
||||
e.a = e.startInstance(e.t, "miA", "127.0.0.1:"+freePort(e.t))
|
||||
e.b = e.startInstance(e.t, "miB", "127.0.0.1:"+freePort(e.t))
|
||||
// 库内服务身份是显式引导(svc init 同款),且为部署级单行:双进程共享
|
||||
// 同一身份,只对 A ensure 一次。账户 CLI 的审计与核验都依赖它。
|
||||
e.identityEnsure(e.t, e.a)
|
||||
}
|
||||
|
||||
// identityEnsure 建立/对齐库内服务身份行(agent_system_identity)。
|
||||
func (e *env) identityEnsure(t *testing.T, inst *instance) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(e.bin, "identity", "ensure", "--instance-id", inst.name, "--data", inst.dataDir, "--dbref", "pg")
|
||||
cmd.Dir = inst.dataDir
|
||||
cmd.Env = append(os.Environ(), "AGENT_SYSTEM_DSN="+e.dbDSN)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("identity ensure %s: %v\n%s", inst.name, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) startInstance(t *testing.T, name, addr string) *instance {
|
||||
@@ -331,11 +346,12 @@ func (e *env) startInstance(t *testing.T, name, addr string) *instance {
|
||||
// 否则测不到带类型空 *S3 回落本地目录的路径。
|
||||
cmd.Env = withoutBlobstoreS3(os.Environ())
|
||||
// 独立 cwd:serve 会向 ./data 写 skills/SQLite 等,不能落在测试目录。
|
||||
cmd.Dir = t.TempDir()
|
||||
dataDir := t.TempDir()
|
||||
cmd.Dir = dataDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start %s: %v", name, err)
|
||||
}
|
||||
inst := &instance{name: name, base: "http://" + addr, cmd: cmd, log: log}
|
||||
inst := &instance{name: name, base: "http://" + addr, dataDir: dataDir, cmd: cmd, log: log}
|
||||
t.Cleanup(func() { e.stopInstance(inst) })
|
||||
e.waitHealthy(t, inst)
|
||||
return inst
|
||||
@@ -434,38 +450,35 @@ func (e *env) killNine(t *testing.T, inst *instance) {
|
||||
|
||||
// loginAlice 引导 alice 账号并发放 API key(CLI 直连测试库;key 存共享 PG,
|
||||
// 对两实例同时有效——程序化验收的首选凭证,SSE 亦可用 ?key= 兜底)。
|
||||
// 当前基座拒绝带密码的 user create:空库先 bootstrap 一位开库管理员,再为
|
||||
// alice 发放激活凭据。凭据兑换后设置 alicePass,然后才能发 API Key。
|
||||
// U3 后本地无密码创建:user create 只签发待激活账号的一次性激活 Key(凭据
|
||||
// 独占落 JSON),redeem 会话进入 grant-setup 改密设 alicePass,完成后再发 Key。
|
||||
func (e *env) loginAlice(t *testing.T) *userClient {
|
||||
t.Helper()
|
||||
// 账户事务会锁库内服务身份;serve 不补写这行,须先显式建立。
|
||||
e.cli(t, "identity", "ensure", "--dsn", e.dbDSN, "--instance-id", "mi-acceptance", "--dbref", "pg")
|
||||
e.cli(t, "user", "bootstrap-admin", "--dsn", e.dbDSN, "--owner", "mi_setup", "--name", "Setup",
|
||||
"--out-file", filepath.Join(t.TempDir(), "setup-grant"))
|
||||
grantFile := filepath.Join(t.TempDir(), "alice-grant")
|
||||
e.cli(t, "user", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "Alice", "--out-file", grantFile)
|
||||
grantRaw, err := os.ReadFile(grantFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 空库先引导首个管理员(本地 pending 创建要求库内已有用户),再建 alice。
|
||||
// 两步都签发一次性凭据,独占落临时 JSON 后即弃。
|
||||
rootCred := filepath.Join(t.TempDir(), "root-cred.json")
|
||||
e.cliIn(t, e.a.dataDir, "user", "bootstrap-admin", "--dsn", e.dbDSN, "--owner", "mi-root", "--out-file", rootCred)
|
||||
activation := filepath.Join(t.TempDir(), "activation.json")
|
||||
e.cliIn(t, e.a.dataDir, "user", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "Alice", "--out", activation)
|
||||
activateKey := activationSecret(t, activation)
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sess := &http.Client{Timeout: httpWait, Jar: jar}
|
||||
// e.uc 尚未赋值 → baseFor 走 A;激活会话 cookie 落 jar 后设置密码。
|
||||
// e.uc 尚未赋值 → baseFor 走 A;redeem 会话(grant-setup)直接设 alicePass。
|
||||
e.do(t, sess, "POST", "/api/auth/login-key/redeem", map[string]any{
|
||||
"key": strings.TrimSpace(string(grantRaw)),
|
||||
"key": activateKey,
|
||||
}, nil)
|
||||
e.do(t, sess, "POST", "/api/auth/password", map[string]any{
|
||||
"next": alicePass,
|
||||
}, nil)
|
||||
|
||||
key := e.cliOut(t, "apikey", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "mi-acceptance")
|
||||
apiKey := e.cliOut(t, "apikey", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "mi-acceptance")
|
||||
uc := &userClient{
|
||||
api: &http.Client{Timeout: httpWait, Transport: bearerRT{key: key}},
|
||||
apiB: &http.Client{Timeout: httpWait, Transport: bearerRT{key: key}},
|
||||
streamB: &http.Client{Transport: bearerRT{key: key}}, // SSE 无整体超时;chan 背压控制
|
||||
api: &http.Client{Timeout: httpWait, Transport: bearerRT{key: apiKey}},
|
||||
apiB: &http.Client{Timeout: httpWait, Transport: bearerRT{key: apiKey}},
|
||||
streamB: &http.Client{Transport: bearerRT{key: apiKey}}, // SSE 无整体超时;chan 背压控制
|
||||
}
|
||||
// runtime.mock 自系统受管设置体制(backend #188/#190)起由 --mock 播种进
|
||||
// 系统层,经 Effective 链对所有租户生效(selectConversationRunner 按
|
||||
@@ -482,6 +495,22 @@ func (b bearerRT) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return http.DefaultTransport.RoundTrip(r2)
|
||||
}
|
||||
|
||||
// activationSecret 读 CLI 凭据独占文件里的激活 Key(JSON {"kind","owner","secret"})。
|
||||
func activationSecret(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v struct {
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &v); err != nil || v.Secret == "" {
|
||||
t.Fatalf("activation credential unreadable: %v", err)
|
||||
}
|
||||
return v.Secret
|
||||
}
|
||||
|
||||
// cliOut 运行 CLI 并取 stdout 首行(apikey create 的明文仅此一次输出)。
|
||||
func (e *env) cliOut(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
@@ -500,9 +529,16 @@ func (e *env) cliOut(t *testing.T, args ...string) string {
|
||||
}
|
||||
|
||||
func (e *env) cli(t *testing.T, args ...string) {
|
||||
t.Helper()
|
||||
e.cliIn(t, t.TempDir(), args...)
|
||||
}
|
||||
|
||||
// cliIn 以指定 cwd 运行 CLI:账户命令经 assertLocalAccountOwner 核验本地实例
|
||||
// 身份,必须从持有该身份的 dataDir 运行。
|
||||
func (e *env) cliIn(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(e.bin, args...)
|
||||
cmd.Dir = t.TempDir()
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("cli %v…: %v\n%s", args[:2], err, out)
|
||||
|
||||
+4
-1
@@ -24,7 +24,10 @@ class TaskLifecycleTests(unittest.TestCase):
|
||||
self.env.update({"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null",
|
||||
"GIT_ALLOW_PROTOCOL": "file", "GIT_TERMINAL_PROMPT": "0",
|
||||
"GIT_AUTHOR_NAME": "Lifecycle Test", "GIT_AUTHOR_EMAIL": "test@example.invalid",
|
||||
"GIT_COMMITTER_NAME": "Lifecycle Test", "GIT_COMMITTER_EMAIL": "test@example.invalid"})
|
||||
"GIT_COMMITTER_NAME": "Lifecycle Test", "GIT_COMMITTER_EMAIL": "test@example.invalid",
|
||||
"GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "gc.auto",
|
||||
"GIT_CONFIG_VALUE_0": "0", "GIT_CONFIG_KEY_1": "maintenance.auto",
|
||||
"GIT_CONFIG_VALUE_1": "false"})
|
||||
for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GITEA_TOKEN", "PRECHECK_RECEIPT",
|
||||
"TASK_PARENT_BRANCH", "TASK_PARENT_PR", "FORCE_ABANDON", "TASK_TARGET_BRANCH",
|
||||
"TASK_WORK_MODE", "VALIDATE_RECEIPT_PURPOSE"):
|
||||
|
||||
Reference in New Issue
Block a user