coder-tools.test.js
5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* coder-tools 单测
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'devloop-ct-'));
process.env.HOME = TEST_HOME;
const pathResolver = require('../../../services/devloop/path-resolver');
const docStore = require('../../../services/devloop/doc-store');
const tools = require('../../../src/tools/devloop/coder-tools');
function setupProject() {
pathResolver._resetForTesting();
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ct-proj-'));
const pid = 'p-codetest';
const reg = {
[pid]: { root_path: root, mode: 'linked-new', company_id: 'c-test' },
};
fs.mkdirSync(path.join(TEST_HOME, '.devloop'), { recursive: true });
fs.writeFileSync(
path.join(TEST_HOME, '.devloop', 'linked-projects.json'),
JSON.stringify(reg)
);
pathResolver._resetForTesting();
return { pid, root };
}
describe('coder-tools', () => {
let env, ctx;
beforeEach(() => {
env = setupProject();
ctx = { pid: env.pid, agent: 'coder' };
});
afterEach(() => {
fs.rmSync(env.root, { recursive: true, force: true });
});
describe('contract_bridge', () => {
function buildContract(exports) {
// hashContract uses contract.exports || contract, so version field stays stable
const c = { exports };
c.version = docStore.hashContract(c);
return c;
}
it('returns ok when hash matches', async () => {
const unitId = 'u-be-auth';
const unitDir = path.join(env.root, 'units', unitId);
fs.mkdirSync(unitDir, { recursive: true });
const contract = buildContract({ http: [{ method: 'POST', path: '/api/login' }] });
fs.writeFileSync(
path.join(unitDir, 'contract.json'),
JSON.stringify(contract, null, 2)
);
fs.writeFileSync(
path.join(env.root, 'units.json'),
JSON.stringify({
units: [{ id: unitId, kind: 'be', contract_hash: contract.version, depends_on: [] }],
})
);
const r = await tools.contract_bridge.execute({ unit_id: unitId }, ctx);
expect(r.ok).toBe(true);
expect(r.hash).toBe(contract.version);
});
it('flags hash mismatch (SDD-declared hash differs)', async () => {
const unitId = 'u-be-mis';
const unitDir = path.join(env.root, 'units', unitId);
fs.mkdirSync(unitDir, { recursive: true });
const contract = buildContract({ http: [{ method: 'GET', path: '/api/x' }] });
fs.writeFileSync(
path.join(unitDir, 'contract.json'),
JSON.stringify(contract)
);
fs.writeFileSync(
path.join(env.root, 'units.json'),
JSON.stringify({
units: [{ id: unitId, kind: 'be', contract_hash: 'sha256:deadbeefdeadbeef', depends_on: [] }],
})
);
const r = await tools.contract_bridge.execute({ unit_id: unitId }, ctx);
expect(r.ok).toBe(false);
expect(r.reason).toMatch(/does not match SDD/);
});
it('returns ok=false when contract missing', async () => {
const r = await tools.contract_bridge.execute({ unit_id: 'u-nope' }, ctx);
expect(r.ok).toBe(false);
expect(r.reason).toMatch(/contract\.json missing/);
});
});
describe('append_coder_log', () => {
it('writes attempt=1 entry without retry fields', async () => {
const r = await tools.append_coder_log.execute({
unit_id: 'u-foo', attempt: 1,
this_fix: 'init', rationale: 'first',
}, ctx);
expect(r.ok).toBe(true);
const content = fs.readFileSync(r.path, 'utf8');
expect(content).toMatch(/attempt: 1/);
expect(content).toMatch(/this_fix: init/);
});
it('rejects retry without last_failure/last_cause', async () => {
await expect(tools.append_coder_log.execute({
unit_id: 'u-foo', attempt: 2,
this_fix: 'try again', rationale: 'hmm',
}, ctx)).rejects.toThrow(/last_failure.*last_cause/);
});
it('accepts retry with required fields', async () => {
const r = await tools.append_coder_log.execute({
unit_id: 'u-foo', attempt: 2,
last_failure: 'auth returns 500',
last_cause: 'user undefined before bcrypt.compare',
this_fix: 'add user null check',
rationale: 'guard against missing user',
}, ctx);
expect(r.ok).toBe(true);
expect(r.attempt).toBe(2);
});
});
describe('sandbox_check', () => {
it('returns ok when no out-of-sandbox writes', async () => {
const unitId = 'u-x';
const unitDir = path.join(env.root, 'units', unitId, 'code');
fs.mkdirSync(unitDir, { recursive: true });
const since = Date.now() - 10_000;
fs.writeFileSync(path.join(unitDir, 'a.js'), 'console.log(1);');
const r = await tools.sandbox_check.execute({ unit_id: unitId, since }, ctx);
expect(r.ok).toBe(true);
expect(r.violations).toEqual([]);
});
it('flags writes outside sandbox', async () => {
const unitId = 'u-y';
fs.mkdirSync(path.join(env.root, 'units', unitId, 'code'), { recursive: true });
const since = Date.now() - 1_000;
// 写一个非沙箱目录文件
const badPath = path.join(env.root, 'rogue', 'evil.js');
fs.mkdirSync(path.dirname(badPath), { recursive: true });
fs.writeFileSync(badPath, 'oops');
const r = await tools.sandbox_check.execute({ unit_id: unitId, since }, ctx);
expect(r.ok).toBe(false);
expect(r.violations.length).toBeGreaterThan(0);
});
});
});